当前位置: 首页 > article >正文

【单片机开发 - STM32(H7)】启动流程、方式、烧录方式详解

如侵权,联系删,个人总结学习用

参考资料:(最末尾有我的原生笔记,那个格式规范点)

安富莱

ARM汇编伪指令详解-CSDN博客

【STM32】STM32内存映射以及启动过程(超详细过程)-CSDN博客

前言:

  • 安富莱:

本章教程主要跟大家讲 STM32H7 的启动过程,这里的启动过程是指从 CPU 上电复位执行第 1 条指令开始(汇编文件)到进入 C 程序 main()函数入口之间的部分

启动过程相对来说还是比较重要的,理解了这个过程,对于以后分析程序还是有些帮助的,要不每次看到这个启动过程都会跳过,直接去看主程序了。还有就是以后打算学习 RTOS 的话,对于这个过程必须有个了解,因为移植的时候涉及到中断向量表。对初学者来说,看这个可能有些吃力,不过不要紧,随着自己做过一些简单的应用之后再来看这章,应该会有很多的帮助,由于我们的 V7 板子是基于 STM32H7XXX,所以我们这里主要针对 H7 系列的启动过程做一下分析,对于 F1, F4 系列也是大致相同的。

  • 我个人:

刚好对中断机制感兴趣,先复习一下启动流程

1. 启动文件

1.1. 不同编译器对应的启动文件

打开我们为本教程提供的工程文件,路径如下:

\Libraries\CMSIS\Device\ST\STM32H7xx\Source\Templates 在这个文件里面有 ST 官方为各个编译器提供的启动文件。

2. 启动文件分析

安富莱:鉴于 V7 开发板使用的是 STM32H743XI,下面我们详细的分析一下启动文件startup_stm32h743xx.s

预备知识

  • 分析前,先掌握一个小技能,遇到不认识的指令或者关键词可以检索。

  • 汇编指令
--AREA伪指令:
	指示汇编程序汇编新的代码节或数据节。
	程序是以程序段为单位组织代码的。
	段是相对独立的指令或代码序列,拥有特定的名称。
	段的种类有代码段、数据段和通用段
		代码段的内容为执行代码
		数据段存放代码运行时需要用到的数据
		通用段不包含用户代码和数据
			所有通用段共用一个空间

	AREA 伪指令用于定义一个代码段或数据段。 
    语法格式:
    AREA 段名 属性 1 ,属性 2 ,……
    其中,段名若以数字开头,则该段名需用 “ | ” 括起来,如 |1_test|
    属性字段表示该代码段(或数据段)的相关属性,多个属性用逗号分隔。常用的属性如下:    
		— CODE 属性:用于定义代码段,默认为 READONLY 
		— DATA 属性:用于定义数据段,默认为 READWRITE 
		— READONLY 属性:指定本段为只读,代码段默认为 READONLY
		— READWRITE 属性:指定本段为可读可写,数据段的默认属性为 READWRITE 
		— ALIGN 属性:使用方式为ALIGN表达式。在默认时, ELF (可执行连接文件)的代码段和数据段是按字对齐的,表达式的取值范围为 0 ~ 31 ,相应的对齐方式为表达式2次方。    
		— COMMON 属性:该属性定义一个通用的段,不包含任何的用户代码和数据。各源文件中同名的COMMON段共享同一段存储单元
		— NOINIT属性:  指定此数据段仅仅保留了内存单元,而没有将各初始值写入内存单元,或者将各个内存单元值初始化为0
		
		一个汇编语言程序至少要包含一个段,当程序太长时,也可以将程序分为多个代码段和数据段。
		使用示例: 
			AREA Init , CODE , READONLY    
			该伪指令定义了一个代码段,段名为 Init ,属性为只读

--SPACE和DCD伪指令:
	SPACE和DCD的功能类似:
		SPACE申请一片内存空间
		DCD申请一个字(32bit)的内存空间。
	SPACE和DCD的区别在于:
		SPACE申请空间但不赋初值
		DCD申请一个字的空间,并赋初值

待补充

2.1. 启动文件由汇编编写,是系统上电复位后第一个执行的程序。主要做了以下工作:

下面先来看启动文件前面的介绍 (固件库版本: V1.2.0)

启动文件是后缀为.s 的汇编语言文本文件, 每行前面的分号表示此行是注释行

启动文件主要完成如下工作,即程序执行过程:

  1. 设置堆栈指针 SP = __initial_sp。(读中断向量表的第一个字,即 RAM 区的栈顶地址,将这个值加载到主栈指针(MSP))
  2. 设置 PC 指针 = Reset_Handler。(然后读中断向量表的第二个字,即复位处理函数地址,跳转到复位处理函数开始执行)
  3. 设置中断向量表。
  4. 配置系统时钟。
  • 配置外部 SRAM/SDRAM 用于程序变量等数据存储【这是可选的】
  1. 跳转到 C 库中的 __main ,最终会调用用户程序的 main()函数。

完整的启动流程图:
┌─────────────────────┐
│     上电/复位       │
└─────────┬───────────┘
          ↓
┌─────────────────────┐
│   加载栈指针(MSP)   │ ← 从0x08000000读取(中断向量表的第一个字)
└─────────┬───────────┘
          ↓
┌─────────────────────┐
│    Reset_Handler    │ ← 从0x08000004读取(中断向量表的第二个字)
└─────────┬───────────┘
          ↓
┌─────────────────────┐
│     SystemInit      │ ← 系统时钟配置(使能FPU、使能HSI、设置向量表位置、配置系统时钟)
└─────────┬───────────┘
          ↓
┌─────────────────────┐
│       __main        │ ← C库初始化
├─────────────────────┤
│    初始化.data段    │
│    清零.bss段       │
│    调用构造函数     │
└─────────┬───────────┘
          ↓
┌─────────────────────┐
│        main         │ ← 用户代码开始
└─────────────────────┘

为什么需要RAM区的栈顶地址的原因:【具体使用看堆栈机制】
- C函数调用需要栈来保存返回地址
- 局部变量需要栈空间
- 中断处理需要栈来保存上下文

内存分布:
Flash (0x08000000):
┌──────────────────┐ 
│   Vector Table   │ 中断向量表
├──────────────────┤
│   .text          │ 代码段
├──────────────────┤
│   .rodata        │ 只读数据
├──────────────────┤
│   .data初始值    │ .data段的初始值
└──────────────────┘

RAM (0x20000000):
┌──────────────────┐ 
│     Stack        │ 栈空间
├──────────────────┤
│     Heap         │ 堆空间
├──────────────────┤
│     .bss         │ 未初始化数据
├──────────────────┤
│     .data        │ 已初始化数据
└──────────────────┘

Cortex-M 内核处理器复位后,处于线程模式,指令权限是特权级别(最高级别),堆栈设置为使用主堆栈 MSP。

2.2. 复位序列

2.3. 代码分析

2.3.1. 【初始化栈】实现开辟栈(stack)空间,用于局部变量、函数调用、函数的参数等

; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
; <h> Stack Configuration
;   <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
// #define Stack_Size      0x00001000
Stack_Size      EQU     0x00001000
// STACK:段名;NOINIT:不初始化;READWRITE:可读可写;ALIGN=3:2^3,即 8 字节对齐
				AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp		// 栈的结束地址,即栈顶地址,需要保存栈顶的地址

第 7 行:EQU 是表示宏定义的伪指令,类似于 C 语言中的#define。

伪指令的意思是指这个“指令”并不会生成二进制程序代码,也不会引起变量空间分配。

Stack_Size 表示宏名

0x00001000 表示栈大小,注意这里是以字节为单位。

第 9 行: 开辟一段数据空间可读可写,段名 STACK,按照 8 字节对齐。

AREA 伪指令表示下面将开始定义一个代码段或者数据段

此处是定义数据段。

ARER 后面的关键字表示这个段的属性。

STACK :表示这个段的名字,可以任意命名。

NOINIT:表示此数据段不需要填入初始数据

READWRITE:表示此段可读可写

ALIGN=3 :表示首地址按照 2 的 3 次方对齐,也就是按照 8 字节对齐(地址对 8 求余数等于 0)。

第 10 行: SPACE 这行指令用于分配一定大小的内存空间,单位为字节

给 STACK 段分配指定字节的连续内存空间,这里指定大小等于 Stack_Size。

第 11 行: __initial_sp 紧接着 SPACE 语句放置,表示了栈顶地址。【指针】(栈区域的最高地址)

__initial_sp 只是一个标号,标号主要用于表示一片内存空间的某个位置,等价于 C 语言中的“地址”概念。

地址仅仅表示存储空间的一个位置,从 C 语言的角度来看,变量的地址,数组的地址或是函数的入口地址在本质上并无区别。

【注意此处栈顶指针还未分配具体地址】【在汇编程序中可以理解为放在哪指哪里的地址】

2.3.2. 【初始化堆】实现开辟堆(heap)空间,主要用于动态内存分配,也就是说用 malloc,calloc, realloc 等函数分配的变量空间是在堆上。

; <h> Heap Configuration
;   <o>  Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
// #define Heap_Size       0x00000800
Heap_Size       EQU     0x00000800
				// HEAP:段名;NOINIT:不初始化;READWRITE:可读可写;ALIGN=3:2^3,即 8 字节对齐
				AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base			// 堆的起始地址
Heap_Mem        SPACE   Heap_Size
__heap_limit		// 堆的结束地址

这几行语句和上面第 1 部分代码类似。

分配一片连续的内存空间给名字叫 HEAP 的段,也就是分配堆空间。堆的大小为 0x00000800。

__heap_base 表示堆的开始地址

__heap_limit 表示堆的结束地址

【注意此处堆指针还未分配具体地址】

2.3.3. 【初始化中断向量表】开辟向量表空间

				PRESERVE8
                THUMB


; Vector Table Mapped to Address 0 at Reset
                AREA    RESET, DATA, READONLY	// RESET:段名;DATA:包含数据,不包含指令;READONLY:只读
                EXPORT  __Vectors
				EXPORT  __Vectors_End
				EXPORT  __Vectors_Size
/* 声明 __Vectors、__Vectors_End 和 __Vectors_Size 这三个标号具有全局属性,可供外部的文件调用 */

第 1 行: PRESERVE8 指定当前文件保持堆栈八字节对齐

第 2 行: THUMB 表示后面的指令是 THUMB 指令集 , CM7 采用的是 THUMB - 2 指令集。

第 6 行: AREA 定义一块数据段,只读,段名字是 RESET。

READONLY 表示只读,缺省就表示代码段了。

第 7-9 行: 3 行 EXPORT 语句将 3 个标号申明为可被外部引用(声明一个标号可被外部的文件使用,使标号具有全局属性)

如果是 IAR 编译器,则使用的是 GLOBAL 这个指令。

主要提供给链接器用于连接库文件或其他文件。【C 文件也可以调用】

2.3.4. 【初始化中断向量表】实现建立中断向量表,中断向量表定位在代码段的最前面。具体的物理地址由链接器的配置参数(IROM1 的地址)决定。

【内存映射】如果程序在 Flash 运行,则中断向量表的起始地址是 0x08000000。

以 MDK 为例,就是如下配置选项:

// __Vectors:向量表起始地址
__Vectors       DCD     __initial_sp               ; 栈顶地址(0x0800 0000),这里只是存了RAM区的栈顶地址,方便单片机启动
                DCD     Reset_Handler              ; 复位程序地址(0x0800 0000+0x04*1)
                DCD     NMI_Handler                ; NMI Handler(0x0800 0000+0x04*2)
                DCD     HardFault_Handler          ; Hard Fault Handler
                DCD     MemManage_Handler          ; MPU Fault Handler
                DCD     BusFault_Handler           ; Bus Fault Handler
                DCD     UsageFault_Handler         ; Usage Fault Handler
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     SVC_Handler                ; SVCall Handler
                DCD     DebugMon_Handler           ; Debug Monitor Handler
                DCD     0                          ; Reserved
                DCD     PendSV_Handler             ; PendSV Handler
                DCD     SysTick_Handler            ; SysTick Handler
				
								中间部分省略未写
				// 外部中断开始
                DCD     WWDG_IRQHandler            ; Window Watchdog
                DCD     PVD_IRQHandler             ; PVD through EXTI Line detect
                DCD     TAMPER_IRQHandler          ; Tamper
                DCD     RTC_IRQHandler             ; RTC
				
				// 限于篇幅,中间代码省略
                DCD     DMA2_Channel2_IRQHandler   ; DMA2 Channel2
                DCD     DMA2_Channel3_IRQHandler   ; DMA2 Channel3
                DCD     DMA2_Channel4_5_IRQHandler ; DMA2 Channel4 & Channel5
				
				// 限于篇幅,中间代码省略
				DCD     0                                 ; Reserved                                    
                DCD     WAKEUP_PIN_IRQHandler             ; Interrupt for all 6 wake-up pins 
// __Vectors_End:向量表结束地址          
__Vectors_End
// 获得向量表大小(字节)
__Vectors_Size  EQU  __Vectors_End - __Vectors

DCD 表示分配 1 个 4 字节的空间。DCD:分配一个或者多个以字(4 字节)为单位的内存,以四字节对齐,并要求初始化这些内存。在向量表中,DCD 分配了一堆内存,并且以 ESR 的入口地址初始化它们。

每行 DCD 都会生成一个 4 字节的二进制代码。

向量表从 FLASH 的 0 地址(一般 0x0800 0000)开始放置,以 4 个字节为一个单位。

  • 地址 0(0x0800 0000)存放的是栈顶地址
  • 0x04 存放的是复位程序的地址
  • 以此类推。

使用:

  • 中断发生:

当异常(也即是中断事件)发生时, CPU 的中断系统会将相应的入口地址赋值给 PC 程序计数器,之后就开始执行中断服务程序。

从代码上看,向量表中存放的都是中断服务函数的函数名,可我们知道 C 语言中的函数名就是一个地址。【中断向量表存放的实际上是中断服务程序的入口地址

2.3.5. 【复位中断程序初始化】实现初始化复位中断服务程序

				AREA    |.text|, CODE, READONLY	// .text:段名;DATA:包含机器指令;READONLY:只读
                
; Reset handler
Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]
	   IMPORT  SystemInit
	   IMPORT  __main
	
                LDR     R0, =SystemInit
	            BLX     R0               
                LDR     R0, =__main
                BX      R0
                ENDP

复位子程序是系统上电后第一个执行的程序,调用 SystemInit 函数初始化系统时钟,然后调用 C库函数 _mian,最终调用 main 函数去到 C 的世界。

第 1 行: AREA 定义一块代码段,只读,段名字是 .text 。 READONLY 表示只读。

第 4 行: 利用 PROCENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。

第 5 行: WEAK 表示弱定义,声明其他的同名标号优先于该标号被引用,就是说如果外面声明了的话会调用外面的。

这个声明很重要,它让我们可以在 C 文件中任意地方放置中断服务程序,只要保证 C 函数的名字和向量表中的名字一致即可。

第 6 行: IMPORT 伪指令用于通知编译器要使用的标号在其他的源文件中定义,表示该标号来自外部文件,跟 C 语言中的 EXTERN 关键字类似。

但要在当前源文件中引用,而且无论当前源文件是否引用该标号,该标号均会被加入到当前源文件的符号表中。

  • 这里表示 SystemInit 和 __main 这两个函数均来自外部的文件

第 9 行: SystemInit 函数(标准库函数)在文件 system_stm32h7xx.c/system_stm32f10x.c 里面,主要实现:

void SystemInit(void)
{
    /* FPU settings */
    SCB->CPACR |= ((3UL << 20)|(3UL << 22));  // 使能FPU
    
    /* Reset the RCC clock configuration */
    RCC->CR |= RCC_CR_HSION;                  // 使能HSI
    
    /* Configure the Vector Table location */
    SCB->VTOR = FLASH_BASE;                   // 设置向量表位置
    
    /* Configure the System clock source */
    SystemClock_Config();                     // 配置系统时钟
}
  • 配置系统时钟、RCC 相关寄存器复位
  • 中断向量表位置设置

第 11 行: __main 标号表示 C/C++标准实时库函数里的一个初始化子程序__main 的入口地址

__main()
{
    // 1. 初始化.data段
    memcpy(__data_start__, __data_load__, __data_end__ - __data_start__);
    
    // 2. 初始化.bss段为0
    memset(__bss_start__, 0, __bss_end__ - __bss_start__);
    
    // 3. 调用构造函数(如果有C++代码)
    __call_init_functions();
    
    // 4. 调用main函数
    main();
    
    // 5. main返回后的处理
    exit(0);
}
  1. 该程序的一个主要作用是初始化堆栈(跳转__user_initial_stackheap 标号进行初始化堆栈的,下面会讲到这个标号)
  2. 初始化映像文件
  3. 最后跳转到 C 程序中的 main 函数

这就解释了为何所有的 C 程序必须有一个 main 函数作为程序的起点。因为这是由 C/C++标准实时库所规,并且不能更改。

2.3.6. 【中断程序初始化】实现其他中断程序初始化

如果使能了中断,中断服务函数实现方法:

  1. 在启动文件中写
  2. 在 C 文件中写(因为是弱定义)
; Dummy Exception Handlers (infinite loops which can be modified)
/* 初始化默认中断程序(无限循环) */
NMI_Handler     PROC
                EXPORT  NMI_Handler                		  [WEAK]
                B       .
                ENDP
HardFault_Handler\
				PROC
				EXPORT HardFault_Handler	 			  [WEAK]
				B .
				ENDP
				// 限于篇幅,中间代码省略
/* 外部中断 */
Default_Handler PROC

                EXPORT  WWDG_IRQHandler                   [WEAK]                                       
                EXPORT  PVD_AVD_IRQHandler                [WEAK]                   
                EXPORT  TAMP_STAMP_IRQHandler             [WEAK]
				// 限于篇幅,中间代码省略
SAI4_IRQHandler
WAKEUP_PIN_IRQHandler

                B       .
	
                ENDP
				
                ALIGN

第 5 行: 死循环,用户可以在此实现自己的中断服务程序。

不过很少在这里实现中断服务程序,一般多是在其它的 C 文件里面重新写一个同样名字的中断服务程序, 因为这里是 WEEK 弱定义的。

如果没有在其它文件中写中断服务器程序,且使能了此中断,进入到这里后,会让程序卡在这个地方。

第 14 行: 缺省中断服务程序(开始)

第 23 行: 死循环, 如果用户使能中断服务程序,而没有在 C 文件里面写中断服务程序的话,都会进入到这里

比如在程序里面使能了串口 1 中断,而没有写中断服务程序 USART1_IRQHandle, 那么串口中断来了,会进入到这个死循环。

第 25 行: 缺省中断服务程序(结束) 。

  • B:跳转到一个标号。这里跳转到一个‘.’,即表示无限循环。
  • PROC:过程(子程序)的开始。
  • ENDP:过程(子程序)的结束。
  • ALIGN:对指令或者数据存放的地址进行对齐,后面会跟一个立即数。缺省表示 4 字节对齐。

2.3.7. 实现用户堆栈初始化(MICROLIB 配置)(两种内存管理模式)

;*******************************************************************************
; User Stack and Heap initialization
;*******************************************************************************
                 IF      :DEF:__MICROLIB	
                 ; MicroLIB模式:更简单的内存管理
                 EXPORT  __initial_sp
                 EXPORT  __heap_base
                 EXPORT  __heap_limit
                
                 ELSE			
				 ; 标准模式:更复杂的内存管理
                 IMPORT  __use_two_region_memory	// 这个函数由用户自己实现
                 EXPORT  __user_initial_stackheap
                 
__user_initial_stackheap
				 ; 设置堆栈的边界
                 LDR     R0, =  Heap_Mem	; 堆的起始地址 -> R0
                 LDR     R1, =(Stack_Mem + Stack_Size)	; 栈顶地址 -> R1
                 LDR     R2, = (Heap_Mem +  Heap_Size)	; 堆的结束地址 -> R2
                 LDR     R3, = Stack_Mem	; 栈的起始地址 -> R3
                 BX      LR					; 返回

                 ALIGN

                 ENDIF

                 END

第 4 行:简单的汇编语言实现 IF…….ELSE…………语句。如果定义了 MICROLIB,那么程序是不会执行 ELSE 分支的代码。 __MICROLIB 可能大家并不陌生,就在 MDK 的 Target Option 里面设置。

MicroLIB是ARM提供的一个精简版C运行时库,专门为资源受限的嵌入式系统设计。

主要优势:

    • 更小的代码体积
    • 更快的执行速度
    • 更简单的实现
    • 更少的RAM使用

标准库与MicroLIB对比:

|特性		|标准库	|MicroLIB	|
|---		|---	|---		|
|代码体积	|大		|小			|
|功能完整性	|完整	|精简		|
|执行速度	|较慢	|较快		|
|内存使用	|较多	|较少		|
|线程支持	|支持	|不支持		|

适用场景:

// 适合MicroLIB的场景	|	// 不适合MicroLIB的场景
- 单任务应用				|	- 需要完整C库功能
- 资源受限系统			|	- 多线程应用
- 实时性要求高			|	- 复杂的格式化输出
- 代码空间受限			|	- 需要文件系统

// MicroLIB不支持或简化的功能
- printf()格式化受限
- scanf()功能受限
- 不支持浮点格式化
- 不支持多线程
- 文件操作受限
- locale支持受限

第 5 行: __user_initial_stackheap 将由__main 函数进行调用。

__user_initial_stackheap是ARM编译器提供的一个函数,用于初始化堆栈空间。

它在不使用MicroLIB时(:DEF:__MICROLIB未定义)被调用。

函数作用:

      • 初始化堆和栈的边界地址
      • 返回四个关键地址(通过R0-R3寄存器):
      • R0: 堆的起始地址
      • R1: 栈顶地址
      • R2: 堆的结束地址
      • R3: 栈的起始地址
高地址
┌──────────────────┐ 
│                  │ ← R1 (Stack_Mem + Stack_Size)
│     Stack        │    栈顶地址
│       ↓          │ 
├──────────────────┤ ← R3 (Stack_Mem)
│                  │    栈底地址
│                  │
│    Free RAM      │
│                  │
│                  │
├──────────────────┤ ← R2 (Heap_Mem + Heap_Size)
│       ↑          │    堆顶地址
│     Heap         │
├──────────────────┤ ← R0 (Heap_Mem)
│                  │    堆底地址
低地址
  • IF,ELSE,ENDIF:汇编的条件分支语句,跟C 语言的if ,else 类似。
  • END:文件结束


 

分析代码后,从中理解——单片机内存地址空间分配

__Vectors,__Vectors_End,__Vectors_Size

__initial_sp

__heap_base,__heap_limit
  • __Vectors,__Vectors_End,__Vectors_Size
定义:
								AREA    RESET, DATA, READONLY
                EXPORT  __Vectors
                EXPORT  __Vectors_End
                EXPORT  __Vectors_Size
使用:
__Vectors       DCD     __initial_sp                      ; Top of Stack
                DCD     Reset_Handler                     ; Reset Handler
								中间部分省略未写                                
                DCD     WAKEUP_PIN_IRQHandler             ; Interrupt for all 6 wake-up pins 
              
__Vectors_End

__Vectors_Size  EQU  __Vectors_End - __Vectors
  • __initial_sp
定义:
Stack_Size      EQU     0x00001000

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp

内存布局:
┌──────────────────┐ __initial_sp (高地址)
│                  │
│    Stack Area    │ Stack_Size (4KB)
│        ↓         │ (向下增长)
└──────────────────┘ Stack_Mem (低地址)
  • __heap_base,__heap_limit
定义:
Heap_Size       EQU     0x00000800												;定义常量

                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

内存布局:
┌──────────────────┐ __heap_limit (高地址)
│        ↑         │
│    Heap Area     │ Heap_Size (2KB)
│                  │ (向上增长)
└──────────────────┘ __heap_base (低地址)
  • 完整的内存布局
┌──────────────────┐ 0x08000000
│   Vector Table   │ __Vectors
│    (...vectors)  │
└──────────────────┘ __Vectors_End

     ... 其他区域 ...

┌──────────────────┐ __initial_sp
│    Stack Area    │ 
│       (4KB)      │
└──────────────────┘ Stack_Mem

     ... 其他区域 ...

┌──────────────────┐ __heap_limit
│    Heap Area     │
│       (2KB)      │
└──────────────────┘ __heap_base

分析代码后,从中理解——中断向量表

分析代码后,从中理解——堆栈机制

何时分配 __initial_sp、__heap_base、__heap_limit 的具体地址
在启动文件中定义的这些符号(__initial_sp、__heap_base、__heap_limit)
在汇编代码中只是占位符,它们的具体地址是在链接阶段由链接器根据链接脚本分配的。

堆和栈的内存区域通常是在编译和链接阶段通过链接脚本固定分配的。
运行时动态调整堆和栈的大小或位置是非常困难的。
堆栈地址分布示意图
┌──────────────────┐ 0x20020000										- Stack Top/Start    : 0x20020000 (__initial_sp)
│                  │
│    Stack Area    │ Stack Size = 2KB (0x800)
│        ↓         │ (向下增长(从高地址向低地址))
├──────────────────┤ 0x2001F800										- Heap End     : 0x2001F800 (__heap_limit)
│        ↑         │
│    Heap Area     │ Heap Size = 2KB (0x800)
│                  │ (向上增长(从低地址向高地址))
├──────────────────┤ 0x2001F000										- Heap Start   : 0x2001F000 (__heap_base)
│                  │
│                  │
│   Other RAM      │ 应用程序数据(124kb)
│                  │
│                  │
├──────────────────┤ 0x20000000
│   Vector Table   │
└──────────────────┘

Memory Details:
- Stack Top    : 0x20020000 (__initial_sp)
- Stack Bottom : 0x2001F800
- Heap Start   : 0x2001F000 (__heap_base)
- Heap End     : 0x2001F800 (__heap_limit)

Notes:
→ Stack: 从高地址向低地址增长
→ Heap : 从低地址向高地址增长
→ 中间预留安全区,防止栈堆冲突

STM32 片内 RAM:

启动流程中的设置中断向量表(在 SystemInit 函数中):

// 在SystemInit中设置向量表位置
void SystemInit(void)
{
    /* Configure the Vector Table location */
    SCB->VTOR = FLASH_BASE;    // 设置向量表位置为Flash起始地址(0x08000000)
    
    // 或者重定位到RAM
    // SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET;  // 例如:0x20000000
}
/* 链接脚本 (.ld文件) */
MEMORY
{
  FLASH (rx)      : ORIGIN = 0x08000000, LENGTH = 128K    /* Flash区域 */
  RAM (xrw)       : ORIGIN = 0x20000000, LENGTH = 64K     /* RAM区域 */
}

SECTIONS
{
  .isr_vector :   /* 向量表段 */
  {
    . = ALIGN(4);
    KEEP(*(.isr_vector))
    . = ALIGN(4);
  } >FLASH
}
                AREA    RESET, DATA, READONLY
                EXPORT  __Vectors
                EXPORT  __Vectors_End
                EXPORT  __Vectors_Size

__Vectors       DCD     __initial_sp               ; 栈顶指针
                DCD     Reset_Handler              ; 复位
                DCD     NMI_Handler                ; NMI
                DCD     HardFault_Handler          ; Hard Fault
                ...
                
__Vectors_End

__Vectors_Size  EQU  __Vectors_End - __Vectors    ; 计算向量表大小
// 1. 定义新的向量表数组
__attribute__((section(".ram_vector"))) 
uint32_t g_pfnVectors[] = {
    (uint32_t)&_estack,            // 栈顶指针
    (uint32_t)&Reset_Handler,      // 复位
    (uint32_t)&NMI_Handler,        // NMI
    (uint32_t)&HardFault_Handler,  // Hard Fault
    ...
};

// 2. 重定位函数
void NVIC_SetVectorTable(void)
{
    // 禁止所有中断
    __disable_irq();
    
    // 复制向量表到RAM
    uint32_t *src = (uint32_t *)&__Vectors;
    uint32_t *dst = (uint32_t *)SRAM_BASE;
    for(uint32_t i = 0; i < __Vectors_Size/4; i++)
    {
        dst[i] = src[i];
    }
    
    // 设置VTOR指向新的向量表
    SCB->VTOR = SRAM_BASE;
    
    // 使能所有中断
    __enable_irq();
}
// 方法1:直接在启动文件中定义
void USART1_IRQHandler(void)
{
    // 中断处理代码
}

// 方法2:动态修改向量表
void SetInterruptHandler(IRQn_Type IRQn, void (*handler)(void))
{
    uint32_t *vectors = (uint32_t *)SCB->VTOR;
    vectors[IRQn + 16] = (uint32_t)handler;
}

3. 程序启动方式&烧录方式

3.1. 启动方式

3.2. 烧录方式

3.2.1. ISP(串口烧录)——基于系统 BootLoader

3.2.2. ICP(SWD/JTAG接口烧录)——烧录算法

3.2.3. IAP——基于开发者的 BootLodader(分为有线/无线 OTA)

IAP 的原理与上面两种有较大区别,这种方式:(根据实际需要由开发者自行分配)

  • 单区 IAP:将主存储区又分成了两个区域
    • 0x0800 0000 起始处的这部分存储一个开发者自己设计的 Bootloader 程序
    • 另一部分存储真正需要运行的 APP 程序
  • 双区 IAP:将主存储区又分成了三个区域
    • 0x0800 0000 起始处的这部分,存储一个开发者自己设计的 Bootloader 程序
    • 一部分存储真正需要运行的 APP 程序
    • 一部分存储接收到的需要更新的 APP 程序

单片机的 Bootloader 程序,其主要作用就是给单片机升级。在单片机启动时,首先从 Bootloader 程序启动,一般情况不需要升级,就会立即从 Bootloader 程序跳转到存储区另一部分的 APP 程序开始运行。

假如 Bootloader 程序时,需要进行升级(比如APP程序运行时,接收到升级指令,可以在 flash 中的特定位置设置一个标志,然后触发重启,重启后进入 Bootloader 程序,Bootloader 程序根据标志位就能判断是否需要升级),则会通过某种方式(比如通过 WIFI 接收升级包,或借助另一块单片机接收升级包,Bootloader 再通过串口或 SPI 等方式从另一块单片机获取升级包数据)先将接收到的程序写入存储区中存储 APP 程序的那个位置,写入完成后再跳转到该位置,即实现了程序的升级。

 https://www.yuque.com/u41716106/ni1clp/ywm4rdb6tve7mg8x?singleDoc# 《【STM32(H7)】启动流程、方式、烧录方式详解》


http://www.kler.cn/a/507045.html

相关文章:

  • 梁山派入门指南4——定时器使用详解,包括定时器中断、PWM产生、输入捕获测量频率
  • windows11下 podman-desktop 复制插件文件 到 RabbitMQ 容器内,并启用
  • React封装倒计时按钮
  • 运行fastGPT 第四步 配置ONE API 添加模型
  • YOLOv8从菜鸟到精通(二):YOLOv8数据标注以及模型训练
  • c#-Halcon入门教程——标定
  • JS Clipboard API
  • 本地部署Web-Check网站检测与分析利器并实现远程访问实时监测
  • python爬虫笔记
  • 当PHP遇上区块链:一场奇妙的技术之旅
  • 【Python】使用python 对excel文件进行加密
  • 基于SpringCloud的广告系统设计与实现(一)
  • vscode 切换文件时,修改内容时很卡,怎么解决?
  • No.33 笔记 | Docker入门:基础概念与实用指南
  • wordpress的火车头商品发布接口
  • iOS面试模版
  • 赤店商城系统点餐小程序多门店分销APP共享股东h5源码saas账号独立版全插件全开源
  • 学习threejs,使用OrbitControls相机控制器
  • STM32的集成开发环境STM32CubeIDE安装
  • spark,读取和写入同一张表问题
  • 掌握 TypeScript 的 `Omit` 工具类型:灵活操作对象属性的艺术
  • [Qt]常用控件介绍-布局管理器-QVBoxLayout、QHBoxLayout、QGridLayout、QFormLayout、QSpace控件
  • AI实验室copilot自动化科研,AMD联手约翰霍普金斯大学:成本节约84%!
  • 【JVM-6】JVM 监控工具 jstat 的使用和具体应用案例
  • 【区间DP】【hard】力扣730. 统计不同回文子序列
  • css3网格布局