【stm32】stm32深入思考(1) 之 系统启动流程 main之前的过程,__main作用,start.s作用

1. 引言

看到这篇文章的应该都是做嵌入式的,都不是新手,可能大家都上手过一些片子,也开发过项目。
用下来感觉如何?
MCU的门槛是很低的,现在的网上资料一大堆,课程满天飞,很多人都可以快速上手,厂家给的SDK也相对完善,可以说这部分很简单。
在这种情况下,只要你懂C语言和一些简单的外设原理,对着demo你就能开发。

在这个基础上,怎么样更深一步,真正的从开发中学到东西?而不是单纯的会抄demo而已?
从我站的不高的角度来分析,我觉得要深入思考一下现象下的本质,一些原理性的东西,底层的东西。

第一篇,先从stm32 系统的启动开始。

2. 程序的入口不是main

最开始学的时候以为main就是所有程序的入口,是大家约好的,后来接触了6410/2440这些,就会有疑惑,为啥stm32不用启动代码,再后来知道这些都是keil和st帮我们做了,很多东西其实我们就一眼带过了,便捷是便捷了,导致的原因可能就是很久都不清楚真正的原因。

那这一篇文章的目标就是:

能想象出一个芯片上电之后的样子,是怎么样跑的,PC怎么动的,堆栈怎么分的,内存是怎么样子

下面我们用stm32f103c8t6,来真实的跑一遍。

3. 系统启动的流程

3.1 上电取址

刚上电,cotex-m3的内核默认会去0x0地址取出栈指针地址,然后偏移4个字节取出跳转地址。
在Cotex m3 权威指南中有介绍。
【stm32】stm32深入思考(1) 之 系统启动流程 main之前的过程,__main作用,start.s作用_第1张图片
这里有个地方要注意:

  1. 是直接取跳转地址,而不是取跳转指令,所以直接把要跳转的地址放在4字节偏移的地方就好。

【stm32】stm32深入思考(1) 之 系统启动流程 main之前的过程,__main作用,start.s作用_第2张图片
如图,可以看出0x8000004的数据是0x80001ED,把0x80001ED填入PC,实际运行的是0x80001EC的代码,为什么不相等?

PC 中的数据最低两位并不代表真实的取址地址。ARM中使用最低一位来判断这条指令是 ARM 指令还是 Thumb 指令,若最低位为 0,代表 ARM 指令;若最低位为 1,代表 Thumb 指令。在 Cortex-M/R 内核中,并不支持 ARM 模式,若强行切换到 ARM 模式会引发一个 Hard Fault。

3.2 地址映射

看到这里可能有朋友不理解,我们的代码明明是load到0x8000000地址的,为什么这里是从0x0开始运行的。
这里,其实是ST做了一个地址的映射,根据boot0,boot1的组合,把0x8000000(flash)映射到了0x0。这其实也就是我们熟悉的启动mode的选择。

【stm32】stm32深入思考(1) 之 系统启动流程 main之前的过程,__main作用,start.s作用_第3张图片
STM32把从0x00000000到0x0005FFFF的区域作为启动空间(boot space)的别名区。

referenc manual
【stm32】stm32深入思考(1) 之 系统启动流程 main之前的过程,__main作用,start.s作用_第4张图片

这里可以看出,ST可以把0x8000000或者片内固化启动代码的地方映射为0。
关于RAM启动,即0x20000000,我看到两种说法,
一种是,0x2000 0000 无法映射到0x0,所以要重设中断向量表。
另一种说法是,0x2000 0000 映射到了0x0,但是在启动后断开了,所以无法通过0x0的地址去访问RAM。
这部分我有点疑惑,后面又专门做了这方面的验证:stm32深入思考(2) 之 RAM启动

引用一段在别的帖子里看到的:

所有的处理器PC指针复位值都会是0,所以一定会在0地址开始执行代码。
这个0地址是个很讲究的东西。可以出现在0地址的东西通常有下面几种:

  1. 片内的SRAM
  2. 外部总线上的NOR FLASH
  3. 片内固化的ROM CODE。每个厂家都会给ROM CODE起个名字,比如三星管这个叫IROM

那么怎么知道map哪个呢,这种情况下CPU还没开始跑,所以只能依赖于硬件逻辑。一般的做法是通过一些外部的管脚来配置,三星平台管这个叫OM。通过这部分的配置,芯片内部逻辑确定将什么东东map过去。

这部分是ST做的,这样,系统上电就会直接到0x8000000,也就是我们下载了bin的地方。

3.3 bin是什么

bin是什么这里就不细说了,就是镜像文件,还有和hex的区别关系什么的,大家应该都清楚,简而言之,就是bin是啥样,flash就是啥样,一模一样的。所以做IAP升级的时候只能升级bin格式进去。
3.2我们说到,系统会从0x800 0000取MSP,从0x8000004取PC的初始值。
而我们的bin就是直接load到0x8000000的,也就是说,编译的东西,放在最前面的是什么?
查生成文件的排列顺序,一般就是直接看链接文件的,但是我在keil里面有找到.ld/.lds文件这些,有点晕,后来在输出文件夹找到一个sct文件,打开是这样的。

; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************

LR_IROM1 0x08000000 0x00010000  {    ; load region size_region
  ER_IROM1 0x08000000 0x00010000  {  ; load address = execution address
   *.o (RESET, +First)
   *(InRoot$$Sections)
   .ANY (+RO)
  }
  RW_IRAM1 0x20000000 0x00004000  {  ; RW data
   .ANY (+RW +ZI)
  }
}

这里rom和ram的配置,和我在option里设置的是一样的。
【stm32】stm32深入思考(1) 之 系统启动流程 main之前的过程,__main作用,start.s作用_第5张图片

但是这里我们要注意一句话,就是:

** *.o (RESET, +First) **

把RESET段放在第一个。
OK,那找到了,RESET段在哪?
搜索一下代码,在start.s (startup_stm32f10x_hd.s)里。
【stm32】stm32深入思考(1) 之 系统启动流程 main之前的过程,__main作用,start.s作用_第6张图片
这一段就是DCD了一些空间,放在最上面的第一二句:

__Vectors       DCD     __initial_sp               ; 
                DCD     Reset_Handler              ; 

就是0x8000000和0x8000004位置的东西。
可以看出,正是一个是MSP(__initial_sp),一个是系统入口( Reset_Handler装入PC)

3.4 start.s文件

这个文件是启动文件,是st公司提供的,根据不同的容量,有startup_stm32f10x_cl.s、startup_stm32f10x_hd.s、startup_stm32f10x_hd_vl.s。。。等等。
内部具体源码的分析,在CSDN有太多太多了,一抓一大把,这里就不细细描述了,直接拿我们要的东西。

  1. 第一个要注意的就是RESET这里,在3.3也说了,中断向量表,是放在最前面的
 AREA    RESET, DATA, READONLY ;复位段,只包含数据,只读
 ...
  1. 第二个要注意就是Reset_Handler ,这是跟我们流程有关系的,刚才可以看到0x8000004位置放的就是Reset_Handler 的入口,及PC会跳到这里来运行
; Reset handler
Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]  
                IMPORT  __main
                IMPORT  SystemInit
                LDR     R0, =SystemInit   ; 装载寄存器指令
                BLX     R0    ; 带链接的跳转,切换指令集           
                LDR     R0, =__main
                BX      R0    ; 切换指令集,main函数不返回
                ENDP
  1. 第三个要注意的是_user_initial_stackheap,这个后面会说到
_user_initial_stackheap 
; 此处是初始化两区的堆栈空间,堆是从由低到高的增长,栈是由高向低生长的,两个是互相独立的数据段,并不能交叉使用。

                 LDR     R0, =  Heap_Mem
                 LDR     R1, = (Stack_Mem + Stack_Size)
                 LDR     R2, = (Heap_Mem +  Heap_Size)
                 LDR     R3, = Stack_Mem
                 BX      LR

3.5 Reset_Handler

start.s里的Reset_Handler 是和流程有关的,系统上电,PC取到Reset_Handler入口,跳转过来,然后运行,看代码。

; Reset handler
Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]  
                IMPORT  __main
                IMPORT  SystemInit
                LDR     R0, =SystemInit   
                BLX     R0               
                LDR     R0, =__main
                BX      R0    
                ENDP

很明显看出,系统是调用了SystemInit 和 __main两个函数。

3.6 SystemInit

SystemInit 是在system_stm32f10x.c中,
看代码


/**
  * @brief  Setup the microcontroller system
  *         Initialize the Embedded Flash Interface, the PLL and update the 
  *         SystemCoreClock variable.
  * @note   This function should be used only after reset.
  * @param  None
  * @retval None
  */
void SystemInit (void)
{
  /* Reset the RCC clock configuration to the default reset state(for debug purpose) */
  /* Set HSION bit */
  RCC->CR |= (uint32_t)0x00000001;

  /* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */
#ifndef STM32F10X_CL
  RCC->CFGR &= (uint32_t)0xF8FF0000;
#else
  RCC->CFGR &= (uint32_t)0xF0FF0000;
#endif /* STM32F10X_CL */   
  
  /* Reset HSEON, CSSON and PLLON bits */
  RCC->CR &= (uint32_t)0xFEF6FFFF;

  /* Reset HSEBYP bit */
  RCC->CR &= (uint32_t)0xFFFBFFFF;

  /* Reset PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE bits */
  RCC->CFGR &= (uint32_t)0xFF80FFFF;

#ifdef STM32F10X_CL
  /* Reset PLL2ON and PLL3ON bits */
  RCC->CR &= (uint32_t)0xEBFFFFFF;

  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x00FF0000;

  /* Reset CFGR2 register */
  RCC->CFGR2 = 0x00000000;
#elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x009F0000;

  /* Reset CFGR2 register */
  RCC->CFGR2 = 0x00000000;      
#else
  /* Disable all interrupts and clear pending bits  */
  RCC->CIR = 0x009F0000;
#endif /* STM32F10X_CL */
    
#if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL)
  #ifdef DATA_IN_ExtSRAM
    SystemInit_ExtMemCtl(); 
  #endif /* DATA_IN_ExtSRAM */
#endif 

  /* Configure the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers */
  /* Configure the Flash Latency cycles and enable prefetch buffer */
  SetSysClock();

#ifdef VECT_TAB_SRAM
  SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#else
  SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */
#endif 
}

主要做了两个事,

  1. 初始化系统时钟(RCC相关)
  2. 重定位了中断向量表,根据是rom还是ram重定位到0x8000000还是0x20000000

3.7 __main

__main这个函数我单步跟踪没有跟进去,暂时看不到是怎么实现的,只能看下汇编:
【stm32】stm32深入思考(1) 之 系统启动流程 main之前的过程,__main作用,start.s作用_第7张图片
观察R0寄存器,说明__main就是在0x8000131这里。
找到对应的位置,可以看到跳转的是__scatterload函数。
为什么不是131而是130,和之前用0x80001ED跳转 0x80001EC的道理一样。

【stm32】stm32深入思考(1) 之 系统启动流程 main之前的过程,__main作用,start.s作用_第8张图片
抄一张经典图
【stm32】stm32深入思考(1) 之 系统启动流程 main之前的过程,__main作用,start.s作用_第9张图片

3.8 __main之__scatterload

这个函数看介绍,作用是:

负责把RW/RO输出段从装载域地址复制到运行域地址,并完成了ZI运行域的初始化工作。

这个函数我没找到源码,但是综合流程,应该就是把flash中bin文件的RW/ZI段等一些运行中需要修改的内容拷贝到RAM,同时把ZI段(我理解就是BSS + heap + stack)清零。

3.9 __main之__rt_entry

负责初始化堆栈,完成库函数的初始化,最后自动跳转向main()函数

3.10 main

OK,到达目的地,成功抵达main函数。

参考资料

  • 《cotex-m3 权威指南》
  • 《STM32 referenc manual》
  • __main解析
  • M内核启动流程
  • 0x8000000映射到0x0

系列文章

  • 【stm32】stm32深入思考(1) 之 系统启动流程 main之前的过程,__main作用,start.s作用
  • 【stm32】stm32深入思考(2) 之 RAM启动

你可能感兴趣的:(stm32)