DKENNY 发表于 2025-2-15 15:49

深入解析 APM32:从上电到主函数的启动之旅

本帖最后由 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,
str r4,
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:
strr3,
adds r2, r2, #4

L_loop1:
cmp r2, r4
bcc L_loop2      上面这一大段代码,就是把 Flash 里的 data 段完整地复制到 RAM 的整个过程,另外还包含了 bss 段的清零工作。
      接下来,在进入 C 空间之前,还有这么一些准备工作:
    blSystemInit
    bl __libc_init_array
bl main
bx lr
.size Reset_Handler, .-Reset_Handler      这里面,blSystemInit 是初始化系统时钟。bl __libc_init_array 是初始化 lib 库。bl main 就是跳转到 main 函数。
      最后就是中断向量表的内容啦:
// This is the code that gets called when the processor receives an unexpected interrupt.
    .section .text.Default_Handler,"ax",%progbits
Default_Handler:
L_Loop_infinite:
b L_Loop_infinite
.size Default_Handler, .-Default_Handler

// The minimal vector table for a Cortex M3.
   .section .apm32_isr_vector,"a",%progbits
.type g_apm32_Vectors, %object
.size g_apm32_Vectors, .-g_apm32_Vectors

// Vector Table Mapped to Address 0 at Reset
g_apm32_Vectors:

.word _end_stack                        // Top of Stack
.word Reset_Handler                     // Reset Handler
.word NMI_Handler                         // NMI Handler
.word HardFault_Handler                   // Hard Fault Handler
.word MemManage_Handler                   // MPU Fault Handler
.word BusFault_Handler                  // Bus Fault Handler
.word UsageFault_Handler                  // Usage Fault Handler
.word 0                                 // Reserved
.word 0                                 // Reserved
.word 0                                 // Reserved
.word 0                                 // Reserved
.word SVC_Handler                         // SVCall Handler
.word DebugMon_Handler                  // Debug Monitor Handler
.word 0                                 // Reserved
.word PendSV_Handler                      // PendSV Handler
.word SysTick_Handler                     // SysTick Handler
.word WWDT_IRQHandler                     // Window Watchdog
.word PVD_IRQHandler                      // PVD through EINT Line detect
.word TAMPER_IRQHandler                   // Tamper
.word RTC_IRQHandler                      // RTC
.word FLASH_IRQHandler                  // Flash
.word RCM_IRQHandler                      // RCM
.word EINT0_IRQHandler                  // EINT Line 0
.word EINT1_IRQHandler                  // EINT Line 1
.word EINT2_IRQHandler                  // EINT Line 2
.word EINT3_IRQHandler                  // EINT Line 3
....      这里面要注意哈,上面这些函数都是 weak 函数,也就是说它们没有实际的函数实体。要是外部有中断触发了,但是没有对应的中断函数,这时候就会发现代码卡在了 Default_Handler 函数里。为啥呢?因为外部没有定义同名函数,那就都会去运行这个缺省的 Default_Handler 函数。
      上面这个中断向量表的完整内容可以在《APM32F103xCxDxE 用户手册 V1.7》里找到,我这里只是截取了一部分。


      startup_apm32f10x_hd.S 这个文件可是系统的启动文件哦,它主要干了这么几件事儿:对堆和栈进行初始化配置、配置中断向量表,还有就是把程序引导到 main( ) 函数里。
      总结一下哈,startup_apm32f10x_hd.S 主要完成了三项工作:初始化栈和堆、确定中断向量表的位置、调用 Reset Handler 。

3 APM32的启动流程实例分析
3.1 Bootloader的作用
      在根据 BOOT 引脚确定好启动方式之后,处理器接下来要做的第二大步,就是从 0x00000000 地址这个地方开始执行代码啦,而这个地址存放的代码就是 Bootloader 。
      Bootloader 呢,也能叫做启动文件。咱得清楚,每一种微控制器(也就是处理器)都得有启动文件。启动文件的作用可不小,它负责执行微控制器从“复位”开始,一直到“开始执行 main 函数”这段时间(我们把这段时间叫做启动过程)里必须要进行的各项工作。就像咱们常见的 51、AVR 或者 MSP430 这些微控制器,它们肯定也都有对应的启动文件。不过呢,这些微控制器的开发环境通常都会自动完整地提供启动文件,开发人员不用再操心启动过程的事儿,直接从 main 函数开始进行应用程序的设计就成。同样的道理,对于 APM32 微控制器,不管是 MDK 还是 IAR 开发环境,Geehy 公司都给咱们准备好了现成的、拿过来就能用的启动文件。
启动文件里首先会做两件事,一个是定义堆栈,另一个是定义中断 / 异常向量表。在这个过程中,只实现了复位的异常处理函数 Reset_Handler 。这个函数的功能可不少,除了初始化时钟、FPU 这些,还有一个特别重要的功能,就是进行内存的搬移和初始化操作。


      咱们都知道,烧录的镜像文件里包含只读代码段.text 、已经初始化的数据段.data ,还有未初始化或者初始值为 0 的数据段.bss 。代码段因为是只读的,所以可以一直放在 Flash 里,CPU 通过总线去读取代码然后执行就没问题。但是.data 段和.bss段涉及到读写操作,为了能有更高的读写效率,就得把它们搬到 RAM 里来执行。所以 Bootloader 会执行很关键的一步,就是在 RAM 里初始化.data 和.bss 段,把相应的内存区域进行搬移或者清空处理。
要是启动方式选的是从内置 Flash 启动,代码还是在 Flash 里执行,不过数据会被拷贝到内部 SRAM 中,这个过程就是由 Bootloader 来完成的。等 Bootloader 把这些流程都处理完了,就会把控制权交给 main 函数,让它开始执行用户代码。
      有了前面这些分析,接下来咱们就具体瞧瞧 APM32 启动流程到底是怎么一回事儿。

3.2 初始化SP、PC、向量表
      系统复位之后呢,处理器会先去读取向量表中的前两个字(也就是 8 个字节)。这第一个字会被存入 MSP 里,第二个字是复位向量,这个复位向量就是程序开始执行的起始地址。


      咱可以通过 J-Flash 打开 hex 文件来看。


      这时候,硬件会自动从 0x0800 0000 这个位置读取数据,然后把它赋值给栈指针 SP 。接着,又会自动从 0x0800 0004 这个位置读取数据,赋值给 PC ,这样复位操作就完成啦。这时候,SP 的值是 0x2002 0000 ,PC 的值是 0x0800 0185 。
      完成 SP 和 PC 的初始化后,紧接着就要初始化向量表啦。要是觉得看 HEX 文件太抽象,不太好理解,那咱就来看看反汇编文件。


      是不是感觉这样更容易懂一些啦?是不是能和用户手册里的向量表对应上了呀?其实看反汇编文件对理解 APM32 的启动流程很有帮助,就是可能稍微有点抽象。

3.3 设置系统时钟
      细心的朋友或许已经察觉到了,PC = 0x0800 0185 这个地址没对齐。再看看反汇编文件,是这样的:


      其实呢,是硬件自动把地址对齐到 0x0800 0185 ,接着就开始执行 SystemInit 函数来初始化系统时钟啦。
      接下来程序就会进到 SystemInit 函数里头。


      SystemInit 函数长这样:
/*!
* @brief       Setup the microcontroller system
*
* @param       None
*
* @retval      None
*
*/
void SystemInit(void)
{
    /* Set HSIEN bit */
    RCM->CTRL_B.HSIEN = BIT_SET;

#ifdef APM32F10X_CL
    RCM->CFG &= (uint32_t) 0xF0FF0000;
#else
    /* Reset SCLKSEL, AHBPSC, APB1PSC, APB2PSC, ADCPSC and MCOSEL bits */
    RCM->CFG &= (uint32_t) 0xF8FF0000;
#endif /* APM32F10X_CL */

    /* Reset HSEEN, CSSEN and PLLEN bits */
    RCM->CTRL &= (uint32_t) 0xFEF6FFFF;
    /* Reset HSEBCFG bit */
    RCM->CTRL_B.HSEBCFG = BIT_RESET;
    /* Reset PLLSRCSEL, PLLHSEPSC, PLLMULCFG and USBDIV bits */
    RCM->CFG &= (uint32_t) 0xFF80FFFF;

#ifdef APM32F10X_CL
    /* Reset PLL2ON and PLL3ON bits */
    RCM->CTRL &= (uint32_t) 0xEBFFFFFF;
    /* Disable all interrupts and clear pending bits*/
    RCM->INT = 0x00FF0000;
    /* Reset CFG2 register */
    RCM->CFG2 = 0x00000000;
#else
    /* Disable all interrupts and clear pending bits */
    RCM->INT = 0x009F0000;
#endif /* APM32F10X_CL */

    SystemClockConfig();

#ifdef VECT_TAB_SRAM
    SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET;
#else
    SCB->VTOR = FMC_BASE | VECT_TAB_OFFSET;
#endif
}      前面这些代码是用来配置时钟的,具体咋回事大家看看手册就行。
      一顿操作下来,最终 PLL 的时钟是 72MHz 。
      这里还有段代码得留意一下:
#ifdef VECT_TAB_SRAM
    SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET;
#else
    SCB->VTOR = FMC_BASE | VECT_TAB_OFFSET;
#endif      默认情况下,VECT_TAB_SRAM 是没开的,这时候就是从 FLASH 启动。VTOR 寄存器存着中断向量表的起始地址,在做 IAP 升级的时候,这里的偏移量会被改掉。

3.4 初始化堆栈并进入main
      执行指令 bl main 后,程序就会跳转到 main 函数啦。当然咯,在这之前呢,会先对 libc 进行初始化。


      到这儿呢,整个启动过程就结束啦。
      最后呀,咱们来总结一下 APM32 从 flash 的启动流程。
      MCU 一上电,就会先从 0x0800 0000 这个位置读取栈顶地址,然后把它保存起来。接着呢,再从 0x0800 0004 这个地方读取中断向量表的起始地址,这个地址就是复位程序的入口地址哦。读取完地址后呢,程序就会跳转到复位程序的入口处,先初始化向量表,之后设置时钟,再设置堆栈。做完这些后,最后就跳转到 C 空间的 main 函数,这也就意味着进入用户程序啦。





页: [1]
查看完整版本: 深入解析 APM32:从上电到主函数的启动之旅