type
slug
status
summary
icon
category
date
tags
password

前言

  • 下面主要讲解从上电复位到 main 函数的过程。主要有以下步骤:
💡
  1. 地址重映射(Memory Remap)
    1. 由硬件自动执行,启动代码(Bootloader),会把复位向量表映射到 Flash 的起始地址0x08000000
  1. 硬件自动初始化 MSP和PC:SP = __initial_spPC = Reset_Handler
  1. SystemInit()
    1. BIOS 硬盘初始化windows 32中主要是时钟树的初始化、SRAM初始化
  1. 调用 C 库函数 _main 初始化用户堆栈,然后进入 main 函数
什么是启动文件?
无论是是何种MCU,从简单的51到ARM9,ARM11,A7 都必须有启动文件,因为嵌入式开发,绝大部分情况都是使用C语言,而C语言一般都是从main函数开始,但是对于MCU来说,它是如何找到并执行main函数的,就需要用到“启动文件”,就是各种 startup_xxxx.s 文件。 启动文件是使用机器认识的汇编语言,经过一些必要的配置,最终能够调用 main 函数,使得用户程序能够在 MCU上正常运行起来的必备文件。
startup_stm32fxxx.s主要完成三个工作:栈和堆的初始化定位中断向量表调用Reset Handler。
 

1 STM32的内存管理

1.1 FLASH和RAM的基本概念

notion image
  • FLASH属于非易失性存储器(掉电不丢失)
    • 又叫做闪存,不仅具备电子可擦除可编程(EEPROM)的性能,还不会断电丢失数据同时可以快速读取数据,U盘和MP3里用的就是这种存储器。
      • 电可擦写:可以通过电信号进行擦写操作,而不需要物理擦除。
      • 可编程:EEPROM中的数据可以根据需要进行修改。
    • 用作存储Bootloader(系统的启动代码)以及操作系统内核或者程序代码或者直接当硬盘使用(U盘)
    • Flash相对容量大,掉电数据不丢失,主要用来存储代码,以及一些掉电不丢失的用户数据。
    • STM32单片机内部的FLASH为 NOR FLASH。
      • NOR Flash的读取和我们常见的SDRAM的读取是一样,用户可以直接运行装载在NOR FLASH里面的代码,这样可以减少SRAM的容量从而节约了成本。
  • RAM(Random Access Memory)属于易失性存储器(掉电丢失)
    • 表示既可以从中读取数据,也可以写入数据。当机器电源关闭时,存于其中的数据就会丢失。
    • RAM有两大类:
      • SRAM:静态RAM(Static RAM/SRAM),SRAM速度非常快,是目前读写最快的存储设备了,但是它也非常昂贵,所以只在要求很苛刻的地方使用,譬如CPU的一级缓冲,二级缓冲。
      • DRAM:动态RAM(Dynamic RAM/DRAM),DRAM保留数据的时间很短,速度也比SRAM慢,不过它还是比任何的ROM都要快,但从价格上来说DRAM相比SRAM要便宜很多,计算机内存就是DRAM的。
        • 这里介绍一种DRAM:
          • DDR RAM(Date-Rate RAM)也称作DDR SDRAM,这种改进型的RAM和SDRAM是基本一样的,不同之处在于它可以在一个时钟读写两次数据,这样就使得数据传输速度加倍了。这是目前电脑中用得最多的内存,而且它有着成本优势,事实上击败了Intel的另外一种内存标准-Rambus DRAM。在很多高端的显卡上,也配备了高速DDR RAM来提高带宽,这可以大幅度提高3D加速卡的像素渲染能力。
    • 为什么需要RAM
      • 因为相对FlASH而言,RAM的速度快很多,所有数据在FLASH里面读取太慢了,为了加快速度,就把一些需要和CPU交换的数据读到RAM里来执行(注意这里不是全部数据,只是一部分需要的数据,这个在后面介绍STM32的内存管理中会提到)。
    • STM32单片机内部的 RAM 为 SRAM。
    • RAM相对容量小,速度快,掉电数据丢失,其作用是用来存取各种动态的输入输出数据中间计算结果以及与外部存储器交换的数据暂存数据
 

1.2 STM32的内存架构

1.2.1 Cortex-M3的存储器映射分析

  • 地址映射
    • 4G的地址空间就是地址编码的范围。所谓编码就是对每一个程序存储器、数据存储器、寄存器和输入输出端口(一个字节)分配一个唯一的地址号码,这个过程又叫做“编址”或者“地址映射”。(这个过程就好像在日常生活中我们给每家每户分配一个地址门牌号。)
  • 寻址
    • 与编码相对应的是“寻址”过程——分配一个地址号码给一个存储单元的目的是为了便于找到它,完成数据的读写,这就是“寻址”,因此地址空间有时候又被称作“寻址空间”。
  • 存储器映射
    • 有了4G的可寻址空间,我们就可通过寻址来操作相应的地址对象。这就需要将程序存储器、数据存储器、寄存器和输入输出端口进行统一编号,也就是存储器映射。
    • 存储器映射是指把芯片中或芯片外的FLASH,RAM,外设,BOOTBLOCK等进行统一编址。即用地址来表示对象。这个地址绝大多数是由厂家规定好的,用户只能用而不能改。用户只能在挂外部RAM或FLASH的情况下可进行自定义。
在《ARM Cotrex-M3权威指南》中有关 M3的存储器映射表:
notion image
💡
  • 存储器映射 是用 地址来表示 对象,因为Cortex-M3是32位的单片机,因此其PC指针可以指向2^32=4G的地址空间,也就是图中的 0x00000000到0xFFFFFFFF的区间,也就是将程序存储器、数据存储器、寄存器和输入输出端口被组织在同一个4GB的线性地址空间内,数据字节以小端格式(低有效字节存储在内存的最低地址处,高有效字节存储在内存的高地址处。)存放在存储器中。
    • 内存地址:内存中的每个字节(8位二进制,即2位16进制)都有一个唯一的地址,地址从低到高递增。
    • eg:假设有一个32位整数 0x12345678 ,在STM32F401的存储器中,它将以小端格式存储,具体如下:32位数据(4个字节),占4个内存地址。每个内存代表一个字节即8位。从表中可以看出,最低有效字节 0x78 存储在最低地址 0x40000000,而最高有效字节 0x12 存储在最高地址 0x40000003
      • 内存地址
        数据字节
        0x40000000
        0x78
        0x40000001
        0x56
        0x40000002
        0x34
        0x40000003
        0x12
💡
代码验证:
一些详细分析:
  • uint8_t p = (uint8_t)&data; uint8_t 是一个无符号的8位整数类型,通常用来表示一个字节。typedef 定义的,通常等价于unsigned char
  • &data&取地址操作符,&data返回变量 data 的内存地址。 例如,如果 data 是一个32位整数,&data 返回的是 data 在内存中的起始地址。例如:0x40000000(这个地址的类型是)
  • 类型转换 (uint8_t*)
 

1.2.2 STM32F103VE 寄存器映射分析

首先,我们对比一下Cortex-M3存储器结构和STM32存储器结构:
notion image
 
图中可以很清晰的看到,STM32的存储器结构和Cortex-M3的很相似,不同的是,STM32加入了很多实际的东西,如:Flash、SRAM等。只有加入了这些东西,才能成为一个拥有实际意义的、可以工作的处理芯片——STM32。
STM32的存储器地址空间被划分为大小相等的8块区域,每块区域大小为512MB。
notion image
notion image
对STM32存储器知识的掌握,实际上就是对Flash和SRAM这两个区域知识的掌握。由STM32的系统结构可以看出,Flash和SRAM这两个区域分别由ICode总线和DCode总线与处理器通信,以此完成相应的数据交换。
notion image
  • STM32的SRAM
    • 不同类型的STM32单片机的SRAM大小是不一样的,但起始地址都是0x2000 0000终止地址都是0x2000 0000+其固定的容量大小
    • STM32F401 SRAM 96KB(0x20000000~0x20017FFF)
    • STM32F103 SRAM 64KB(0x20000000~0x2000FFFF)
    • SRAM的理解比较简单,其作用是用来存取各种动态的输入输出数据、中间计算结果以及与外部存储器交换的数据和暂存数据。设备断电后,SRAM中存储的数据就会丢失。
  • STM32的Flash(512K)
    • STM32的Flash,严格说,应该是Flash模块。该Flash模块包括:Flash主存储区(Main memory)、Flash信息区(Informationblock),以及Flash存储接口寄存器区(Flash memory interface)三个组成部分分别在0x0000 0000——0xFFFF FFFF不同的区域,如下表所示。
    • notion image
      STM32的闪存模块由:主存储器、信息块和闪存储器块3部分组成。
    • 主存储器,该部分用来存放代码和数据常数(如加const类型的数据)。对于大容量产品,其被划分为256页,每页2K,注意,小容量和中容量产品则每页只有1K字节。主存储起的起始地址为0X08000000,B0、B1都接GND的时候,就从0X08000000开始运行代码(见下面启动模式选择)。
    • 信息块,该部分分为2个部分,其中启动程序代码,是用来存储ST自带的启动程序,用于串口下载,当B0接3.3V,B1接GND时,运行的就这部分代码。用户选择字节,则一般用于配置保护等功能。
    • 闪存储器块,该部分用于控制闪存储器读取等,是整个闪存储器的控制机构。
    • 对于主存储器和信息块的写入有内嵌的闪存编程管理;编程与擦除的高压由内部产生。
      在执行闪存写操作时,任何对闪存的读操作都会锁定总线,在写完成后才能正确进行,在进行读取或擦除操作时,不能进行代码或者数据的读取操作。

1.2.3 Cortex-M4的存储器映射分析

notion image
可以发现和Cortex-M3的存储器映射分析没什么区别。
notion image
512KB的Flsh,96KB的SRAM。
  • System Memory (0x1FFF 0000 - 0x1FFF 77FF)这个区域包含了系统内存,主要用于存储引导加载程序(Bootloader)和其他启动代码。在芯片上电或复位时,处理器通常会从这里启动。
  • Flash Memory (0x0800 0000 - 0x0807 FFFF):这里是存储代码的 Flash 存储区域。代码和常驻数据通常存储在此这个区域。
  • SRAM (0x2000 0000 - 0x2001 7FFF)这是静态随机存取存储器(SRAM)区域,用于存放程序运行时的数据、堆栈和局部变量。STM32F401RE 提供了 96KB 的 SRAM。
  • Peripherals (0x4000 0000 - 0x5003 FFFF)这部分是外围设备的地址空间。
  • Cortex-M4 Internal Peripherals (0xE000 0000 - 0xE00F FFFF)这部分用于 Cortex-M4 内核相关的外设,如中断控制器、调试接口等。这些是处理器内部的硬件功能,帮助管理中断、调试和其他控制任务。
  • Reserved (一些地址范围):一些地址区域被保留(Reserved),用于未来的扩展或者其他内部硬件用途,这些区域通常不可用,也不应访问。
  • Option Bytes (0x1FFF C000 - 0x1FFF C007)这个区域存储选项字节,通常用于配置微控制器的一些特性,如启动配置、保护配置等。
  • Boot Configuration (0x0000 0000 - 0x0000 FFFF)这部分与启动引导(Boot)相关,决定了芯片是从 Flash 启动,还是从 SRAM 或系统内存启动。这通常是通过 BOOT 引脚来选择。

1.2.4 STM32F401RE 寄存器映射分析(STM32存储器映射)

notion image
…………………………(省略的很多外设地址查阅Reference manualF401 P38)
notion image
外设基地址为0x4000 0000 ,寻址外设即基地址+偏移量。
那么我们所需要分析的STM32 内存,就是图中 0X0800 0000开始的 Flash 部分 和 0x2000 0000 开始的SRAM部分,这里还要介绍一个和Flash模块相关的部分(见上面)
 

1.3 STM32的内存管理

STM32的内存管理起始就是对0X08000000开始的Flash部分和0x20000000开始的SRAM部分使用管理。
💡
应用程序的组成:
  • 汇编语言角度:数据段、堆栈段、代码段、扩展段
  • 高级语言如C语言:代码段、数据段、BSS 段、栈、堆
  • 我们可以看到一个可执行程序至少包含:代码段 + 数据段 + BSS 段
    • 一般情况下,一个可执行二进制程序(在 linux 下为一个进程单元),在存储时(没有加载到内存运行),至少拥有三个部分,代码段 + 数据段 + BSS 段
    • 当应用程序运行时(运行态),此时需要另外两个域:堆和栈。正在运行的程序:代码段 + 数据段 + BSS 段 + 堆 + 栈
  • 如图所示为可执行应用程序存储态运行态的结构对照图。一个正在运行的 C 程序占用的内存区域分为代码段、数据段(初始化数据)、BSS 段(未初始化数据)、堆和栈 5 部分
    • notion image
  • 静态分配和动态分配
    • 动态分配:在运行时执行动态分配。需要程序员显示管理,通过malloc申请,并且需要手动free掉,否则会造成内存泄漏。
    • 静态分配:在编译时就已经决定好了分配多少 Text+Data+Bss+Stack(静态分配)。静态分配的内存在进程结束后由系统释放(Text+Data),Stack 上的数据则在退出函数后立即被销毁。

1.3.1 C语言的内存分区

notion image
  • 栈(Stack)
    • 栈区由编译器自动分配释放,由操作系统自动管理,无须手动管理。
    • 生命周期:栈区上的内容只在函数调用期间存在,当函数运行结束,这些内容也会自动被销毁。
    • 生长方向和最大大小:按内存地址由高到低方向生长,其最大大小由编译时确定,速度快,但自由性差,最大空间有限。
    • 存放内容:临时创建的局部变量、函数调用时其入口参数、函数返回时其返回值、const 定义的局部变量。也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)
    • 栈区是先进后出原则(LIFO),其操作方式数据结构中的栈是一样的。
  • 堆(Heap)
    • 程序员手动管理:malloc等函数实现动态分布内存,不过它的存储空间一般是不连续的,所以会产生内存碎片。有malloc函数分布的内存,必须用free进行内存释放,否则会造成内存泄漏。
    • 生命周期:mallocfree之间
    • 生长方向和最大大小:堆区按内存地址由低到高方向生长,其大小由系统内存/虚拟内存上限决定,速度较慢,但自由性大,可用空间大。
    • 存放内容:存放程序运行中被动态分布的内存段,可增可减。
    • 底层通过链表或块管理。
  • 全局/静态存储区(数据区)(Static Data Segment)
    • 在程序中,所有静态变量和全局变量,无论是否初始化,都要在程序加载时被分配到内存中。
    • 全局区由 .bss 段和 .data 段组成,可读可写。
    • 通常是用于存储在编译期间就能确定存储大小的变量的存储区,但它用于的是在整个程序运行期间都可见的全局变量和静态变量。
    • .bss 段 ——未初始化
      • 存放未初始化的全局变量和未初始化的静态变量(全局静态变量和局部静态变量)。
      • 存放初始化为0的全局变量和初始化为0的静态变量。
      • .bss段 不占用可执行文件空间,其内容由操作系统初始化。
    • .data段 ——已初始化
      • 存放已初始化的(非0的)全局变量和静态变量
      • .data段 占用可执行文件空间,其内容由程序初始化。
    • 注意
      • .bss段只占运行时的内存空间而不占文件空间。在程序运行的整个周期内,.bss段的数据一直存在
      • 编译时,.bss 区会在编译后的 .map 文件中被标记出来。它会告诉链接器这块区域的大小和地址。加载时,操作系统在加载程序到内存时,会为.bss区分配内存,并将其自动初始化为0。
      • .bss 区节省空间:.bss 区并不实际存储数据(它是未初始化的),编译器在生成可执行文件时并不会把它的内容写入磁盘文件。它只会记录 .bss 区的大小内存位置,实际的零值初始化是在程序加载时由操作系统完成的。
  • 常量区(从下面可以看出来是代码区的一部分,或者说它存储在代码区的只读数据段。)
    • 编译期间就能确定存储大小的常量(不允许被修改
    • 在程序运行期间,存储区内的常量是全局可见的。
    • 存放内容常量字符串、数字、const修饰的全局变量
      • 一些C知识:const 关键字用于声明常量变量,表示该变量的值在程序运行过程中不能被修改。const变量是只读的。
      • 指针和const
        • const int *ptr = &a; 指针所指向的值不能修改,但指针可以改变(即指向其他地方)
        • int * const ptr = &a; 指针不可改变(即不能指向其他地方),但指针所指向的值可以修改
    • 常量数据段叫做 .rodata,即read only,表示常量数据是不可修改的。一旦程序中对其修改将会出现段错误
      • 程序中的常量不一定就放在 .rodata 中,有的立即数和指令编码放在 .text 中
      • 对于字符串常量,若程序中存在重复的字符串,编译器会保证只存在一个
      • .rodata 是在多个进程间共享的
      • 有的嵌入式系统,.rodata 放在 ROM(或者 NOR FLASH)中,运行时直接读取无需加载至 RAM。想要将数据放在 .rodata 只需要加上 const 属性修饰即可。
  • 代码区(Text Segment)
    • 存储程序执行代码(即存放所有的程序指令)
    • 一般是只读的,防止程序在运行时修改代码内容。
    • 存放不可修改的常量数据,例如字符串常量("Hello, World!")和 const 修饰的常量数组、宏定义常量。
  • 总结
    • 一个程序本质上都是由bss段、data段、text段三个组成的。
    • 在C/C++程序编译完成之后,已初始化的全局变量保存在data段中,未初始化的全局变量保存在bss段中。
    • text和data段都在可执行文件中(在嵌入式系统里一般是固化在镜像文件中),由系统从可执行文件中加载;而bss段不在可执行文件中,由系统初始化。
    • 假设现在有一个程序,它的函数调用顺序如下:main(...) -> func_1(...) -> func_2(...) -> func_3(...),即主函数 main 调用函数 func_1; 函数 func_1 调用函数 func_2; 函数 func_2 调用函数 func_3。当一个程序被操作系统调入内存运行, 其对应的进程在内存中的映射如下图所示:
 
notion image
 

1.3.2 单片机存储分配

STM32 的 RO 区域不需要加载到 SRAM,内核直接从 FLASH 读取指令运行。
  • 存储器
    • RAM:RAM 是与CPU直接交换数据的内部存储器,也叫主存(内存),随时读写,掉电是会丢失。DRAM和SRAM(见前面)
    • ROM:只读存储器,只能从里面读出数据而不能任意写入数据。ROM与RAM相比,具有读写速度慢的缺点。但由于其具有掉电后数据可保持不变的优点,因此常用也存放一次性写入的程序和数据,比如主版的BIOS程序的芯片就是 ROM 存储器。
    • Flash:由于ROM具有不易更改的特性,后面就发展了Flash Memory。Flash Memory 不仅具有 ROM 掉电不丢失数据的特点,又可以在需要的时候对数据进行更改,不过价格比 ROM 要高。
  • 由前面的分析我们知道,代码区和常量区的内容是不允许被修改的,ROM(STM32 就是 Flash Memory)也是不允许被修改的,所以代码区常量区的内容编译后存储在ROM中。而全局区.bss段.data段)都是存放在RAM中。
    • 以STM32F407为例
      • notion image
      • ROM(Flash)区域是0x8000000 开始,大小是 0x10000,这片区域是只读区域,不可修改,存放代码区常量区
      • 第一个 RAM 区域是0x20000000 开始,大小是 0x2000,这片区域是可读写区域,存放的是全局(静态)区堆区栈区
    • STM32F401如下
      • notion image
      • ROM(Flash)区域是0x8000000开始,大小是 0x80000 (512KB),这片区域是只读区域,不可修改,存放代码区
      • SRAM区域是0x20000000开始,大小是0x18000(96KB),这片区域是可读写区域,存放的是全局(静态)区堆区栈区
notion image
  • 3种存储属性区和6个存储数据段
    • RO(Read Only)
      • 烧写到Flash中,可以长久保存。.text代码段和.const_data都属于RO。由于需要掉电储存,RO里也保存了一份data的数据。
    • RW(Read Write)
      • 储存在RAM中,.data属于此区。上电时单片机会将Flash中保存的data类型数据复制到RAM中,以供读写使用。
    • ZI(Zero Init)
      • 零初始化区,同样储存在RAM里。系统上电时会把此区域的数据进行0初始化。.bssheap,stack均属于这个区域.
💡
STM32的RAM上有RW和ZI两个属性区,里边包含了data,bss,heap,stack这几个数据段。这里是程序运行的所在。
💡
STM32的Flash中有RO(Read-Only)区,包含了text、constdata和data三个段,这里则是程序本体所在。
MDK编译的结果
notion image
  • RAM = RW-data + ZI-data
    • RW-data:已初始化的数据区,ZI-data:未初始化的数据区和heap、stack(系统会将其初始化为0)
    • RAM = data + bss
  • ROM = Code + RO-data + RW-data
    • Code:代码区;RO-data:只读数据区;RW-data:一些初始化的数据(放在只读里面减少内存使用)
    • Flash = Code + Data
  • bin文件大小:Code + RO data + RW data 
  • 可以看到对于RAM空间,程序启动时首先需要把Flash种的RW-data复制到RAM中,然后把ZI-data加载到RAM中
    • notion image
      对应到具体内存上,结合启动流程如右图:
      程序在存储状态时,RO section 及 RW Section 都被保存在 ROM 区。当程序开始运行时,内核直接从 ROM 中读取代码,并且在执行主体代码前,会先执行一段加载代码,它把 RW Section 数据从 ROM 复制到 RAM,并且在 RAM 加入 ZI Section,ZI Section 的数据都被初始化为 0。加载完后 RAM 区准备完毕,正式开始执行主体程序。
      RW-data 和 ZI-data 它们仅仅是初始值不一样而已为什么需要区别开:
      因为在 RAM 创建数据的时候,默认值为 0,但如果有的数据要求初值非 0,那就需要使用 ROM 记录该初始值,运行时再复制到 RAM。
notion image
因此,让一个程序正常运行:
  • 芯片的 Flash 大小 要大于 Code + RO-data + RW-data 的大小
  • 芯片的 RAM 大小 要大于 RW-data + ZI-data 的大小
  • 补充小知识:ZI-data和编译器优化
    • 在MDK中(其实是ARM Compiler中),默认情况下,所有小于8个字节、本应放在.bss段的ZI-Data,都会被移到.data段作为普通RW-Data。之所以这么做是因为编译器觉得:通过循环赋值的方法给这帮小变量初始化成0太不划算了,初始化他们的程序都比变量本身还大,干脆放几个0到RW的初始值表里,由RW数据的初始化程序顺手处理好了——说了这么多,如果不好理解,简单理解成出于优化的目的就行了。
    • 要想关闭这个优化,在命令行中加入“--bss_threshold=0” 就可以了。顺便说下,默认设置相当于“--bss_threshold=8”。
    • .data section和.bss section是两个默认的section,我们还可以定义自己的 section,并自己指定将哪些变量放到里面。具体怎么实现,请查阅对应编译器的使用手册。记住:变量和代码都是放在段里面的,段具体放在哪里(什么地址上)则是由 linker 的脚本控制的
    • 在MDK中(也就是 ARM Compiler中),这个脚本叫做scatter-loading file;在 IAR和GCC也有对应的 LinkerScript,只不过语法规则不同,感兴趣的人可以查阅对应的手册。

1.3.3 实例代码

编译后map文件中内存分配如下:
notion image
运行程序,打印信息如下:
notion image
data是初始化的全局变量,在.data区;buffer是未初始化的全局变量,在.bss区;pHeap是通过malloc分配的空间,在堆区;局部变量都在栈区。
notion image
 

2 STM32的启动模式

启动模式决定了中断向量表的位置,STM32有三种启动模式:
notion image
  • 主闪存存储器启动
    • 从 STM32 内置的 Flash 启动(0x08000000 − 0x0807FFFF),一般我们使用 JTAG 或者 SWD 模式下载程序时,就是下载到这个里面,重启后也直接从这启动程序。
    • 以 0x08000000 对应的内存为例,则该块内存既可以通过 0x00000000 操作也可以通过 0x08000000 操作,且都是操作的同一块内存。
  • 系统存储器启动
    • 从系统存储器启动(0x1FFFF000 − 0x1FFFF7FF),这种模式启动的程序功能是由厂家设置的。一般来说,我们选用这种启动模式时,是为了从串口下载程序,因为在厂家提供的 ISP 程序中,提供了串口下载程序的固件,可以通过这个 ISP 程序将用户程序下载到系统的 Flash 中。
    • 以 0x1FFFFFF0 对应的内存为例,则该块内存既可以通过 0x00000000 操作也可以通过 0x1FFFFFF0 操作,且都是操作的同一块内存。
  • 片上 SRAM 启动
    • 从内置 SRAM 启动(0x20000000-0x3FFFFFFF),既然是 SRAM,自然也就没有程序存储的能力了,这个模式一般用于程序调试。
    • SRAM 只能通过 0x20000000 进行操作,与上述两者不同。从 SRAM 启动时,需要在应用程序初始化代码中重新设置向量表的位置。

2.1 启动模式的核心作用

  • 决定的是CPU上电或复位后从哪个存储区域开始执行代码
  • 芯片将选定的存储区域(Flash、SRAM或系统存储器)临时映射到0x00000000地址,使得CPU从该地址开始取指令。

2.2 地址重映射机制(Address Remapping)

  • 一句话理解地址重映射:就是把实际内存中的一块区域“临时搬家”,让 CPU 在访问特定地址(比如 0x00000000)时,其实是在访问别的地方(比如 0x08000000)。
  • 核心是硬件层面的地址逻辑映射,而非物理上的代码复制。
    • 无需数据移动:存储器的物理内容仍保留在原地址(如Flash在0x08000000),只是CPU访问0x00000000时被重定向到目标物理地址。
notion image
notion image

notion image

notion image

notion image

总结一波
问题
解答
地址重映射是什么?
把某块物理内存区域“映射”到 0x00000000,CPU 从这里取指令。
为什么要有它?
因为 CPU 固定从 0x00000000 启动,需要根据不同情况让不同内容“住进”这个门牌号。
是谁控制的?
BOOT 引脚控制,系统硬件逻辑实现。
映射完能不能改?
可以,运行中你可以通过 SCB->VTOR 修改向量表位置,软实现“再次重映射”。
0x08000000 和 0x00000000 是不是同一块内存?
启动模式设定为从 Flash 启动时,是的,访问的是同一块。

 

3 STM32启动文件分析

  • 程序的入口函数main函数
  • 在主程序运行之前,需要先运行一个启动文件xxx.s
  • 对于STM32F401RETX来说,汇编文件为:startup_stm32f401xx.s(不同的芯片型号使用的汇编文件不一样)
notion image
描述:STM32F401xx系列向量表针对于MDK-ARM工具链
💡
启动文件的功能如下
  • 初始化堆栈指针SP
  • 初始化PC指针,指向Reset_Handler(复位程序的入口)
  • 初始化中断向量表(vector table)和异常中断服务地址(exception ISR (Interrupt Service Routine) addresses)
  • 配置(configure)系统时钟
  • 跳转到c库中__main函数()【调用一个main()即然后跳转到一个叫做main的函数入口】
startup_stm32f40_41xxx.s:STM32F40xxx/41xxx devices ……with external SRAM (外部SRAM) mounted(挂载命令)。main()入口名字可以更改但不建议,标准库统一入口为main,便于移植。
💡
异常中断服务程序地址
在嵌入式系统和微控制器编程中,异常中断服务程序(ISR)地址是指当发生异常或中断时,处理器跳转到的内存位置。这些地址指向处理不同异常或中断的具体例程。
工作原理
当触发异常或中断时,处理器使用中断向量表(IVT)或中断描述符表(IDT)来查找相应的ISR地址。该表包含条目,将每个可能的异常或中断映射到其对应的处理程序。
ARM架构
在基于ARM的系统中,向量表通常从地址0x00000000开始,包含各种异常和中断的条目。例如:
  • 复位处理程序:位于0x00000004
  • NMI处理程序:位于0x00000008
  • 硬故障处理程序:位于0x0000000C
  • SysTick处理程序:位于0x0000003C
自定义
在许多系统中,向量表可以由程序员自定义。例如,在使用CMSIS(Cortex微控制器软件接口标准)进行嵌入式开发时,向量表通常在启动文件中定义或由开发环境自动处理。

3.1 Stack Configuration 配置栈空间

notion image
;分配栈空间以字节为单位
;根据你的应用需求去调节这个值(tailor…to…使适应)
栈的作用是用于局部变量,函数调用,函数形参等的开销,栈的大小不能超过内部 SRAM 的大小。如果编写的程序比较大,定义的局部变量很多,那么就需要修改栈的大小。
这段代码的含义如下:开辟栈的大小为 0X00000400(1KB),名字为 STACK, NOINIT 即不初始化,可读可写, ALIGN=3 表示 8(2^3)字节对齐。
💡
EQU:对一个常量起一个名字 (相当于#define) 1KB(1024字节)
Stack_Size EQU 0x00000400
notion image
 
AREA:定义一个代码段/数据段
AREA STACK, NOINIT, READWRITE, ALIGN=3
  • 申请一块区域,名字叫做STACK
  • NOINIT 不初始化,即指定本数据段仅仅保留了内存单元,而没有将各初始写入内存单元,或者将内存单元值初始化为0;
  • READWRITE:申请的这块区域可读可写
  • ALIGN:通过添加补丁字节使得当前的位置满足一定的对齐方式(取值为2的n次幂;写3则为八字节对齐。没写则采用默认的字节对齐方式,默认为4字节)
    • notion image
 
SPACE:分配一块内存空间,并用0初始化
Stack_Mem SPACE Stack_Size
  • 分配一块初始化为0的内存空间,大小为Stack_Size(1KB),名字为Stack_Mem
 
__initial_sp
  • 指向栈空间的结束地址(栈顶地址)
  • 栈空间分配为动态分配(即指在程序运行时,根据需要动态地调整栈的大小。)
  • 自上向下增长(向下增长):栈的生长方向是从高地址向低地址扩展。在ARM架构中,栈默认是向下增长的,即从高地址向低地址方向增长。这意味着在ARM系统中,栈顶指针(SP)初始时指向栈底(高地址),随着数据的压入,SP会逐渐减小,指向更低的地址。
  • 栈空间是由编译器自动分配和释放的,栈空间一般存储局部变量、函数调用过程中传递的参数、保护现场。启动文件中的栈空间默认为1KB,当程序比较大,需要用到的局部变量比较多,就必须修改栈空间的大小。如果栈空间溢出,会让程序出现未知问题。(相当于Linux开发中的段错误)
  • 注意:栈空间可以由用户手动调整,但是不能超过内部SRAM的大小(92KB)(闪存Flash为512K)

3.2 Heap Configuration 配置堆空间

notion image
💡
  • Heap_Size EQU 0x00000200
    • 堆空间:0x00000200 512B(栈空间:0x00000400 1KB)
  • AREA HEAP, NOINIT, READWRITE, ALIGN=3
    • 申请一块区域叫做堆,不初始化,可读可写,按8字节对齐
  • __heap_base 表示堆的起始地址
  • __heap_limit表示堆的结束地址。(堆是由低向高生长的,跟栈的生长方向相反。)
  • PRESERVE8 遵循8字节对齐
  • THUMB
    • 表示后面指令兼容THUMB指令集(是ARM以前的指令集),THUMB指令集是16bit(位)。
    • 现在Cortex-M系列的都使用THUMB-2令集,THUMB-2是32位的,兼容16位和32 位的指令,是THUMB的超集。
  • 堆空间一般是动态分配,由用户分配,如Linux系统学习的malloc函数,但堆空间在STM32中使用较少。
 

3.3 配置中断向量表(Vectors

向量表指的是中断源表,向量其实就是内核发送异常的源头。
notion image
notion image
  • AREA RESET, DATA, READONL:这里定义一个数据段,名字为RESET,可读。
  • EXPORT:声明这个标号可以被外部文件调用。相当于声明了一个全局变量。=extern
    • 如果是 IAR 编译器,则使用的是 GLOBAL 这个指令。
    • 声明 __Vectors__Vectors_End 和 __Vectors_Size 这三个标号具有全局属性,可供外部的文件(C 文件等)调用。
  • DCD:分配一块字内存单元,后面跟随的为向量源的地址(即中断的名字)
    • 分配一个或者多个以字为单位的内存,以四字节对齐(四个字节为一个单元。32位为一个字),并要求初始化这些内存。以ISR的入口地址初始化它们
      • DCD 0x08000000 :就相当于在该指令地址分配 4 字节空间,并写入常量 0x08000000DCD Reset_Handler该位置存放复位函数的地址。
    • 当内核响应了一个发生的异常后,对应的异常服务例程(ISR)就会执行。为了决定 ESR 的入口地址,内核使用了向量表查表机制。这里使用一张向量表。
    • 向量表
      • 中断源表,向量其实就是内核发送异常的源头。告诉CPU每个中断或异常发生时应该跳到哪里执行代码。
      • 本质上一个连续的32位数的数组,每个数组元素对应一种异常,存放一种异常的入口地址。
      • 向量表在地址空间中的位置是可以设置的,通过NVIC中的一个重定位寄存器来指出向量表的地址。
        • 在复位后,该寄存器的值为0,STM32的默认启动模式是将Flash 映射到地址 0x00000000。因此,在地址0(即 FLASH 地址 0x00000000)处必须包含一张向量表,用于初始时的异常分配。
        • 在ARM Cortex-M中规定CPU上电或复位后,会自动从地址 0x00000000 开始执行。
        • SCB->VTOR(Vector Table Offset Register)
          • 这样做以后,CPU 不再从 0x00000000 查向量表,而是从 0x20000000 查。
          • 重点!这个重定向是程序运行之后才起作用!在执行这句代码SCB->VTOR = xxx;之前:CPU 是从默认的 0x00000000 开始读取向量表的。所以复位后,地址 0x00000000 处必须放置一个有效的向量表(最起码前两项),才能让程序跑起来。可以在进入 main() 函数后再重定向向量表,实现在 RAM 里动态修改 ISR(中断服务函数)入口地址等功能。
      • 要注意的是这里有个另类:
        • 向量表是一个连续的 32 位数组,每个条目是一个地址。
        • 第0项(即地址 0x00000000 的内容)并不是什么入口地址,而是给出了复位后主堆栈指针(MSP)的初值
        • 真正的“程序入口”其实在向量表的第二个元素,也就是 0x00000004 的位置。
      • 下图是 F401 的向量表的一部分,具体参阅想要使用的芯片对应的手册即可。
        • notion image
          notion image
          下图是 F407 的向量表的一部分
          notion image
0x00000000存储的是栈顶指针,0x00000004存储的是复位程序的地址。以4个字节为一个单位,以此类推。从代码上看,向量表中存放的都是中断服务函数的函数名,可我们知道C语言中的函数名就是一个地址。
notion image
  • __Vectors_Size EQU __Vectors_End - __Vectors
    • 向量表的大小=向量的结束地址-向量的开始地址
 

3.3.1 中断响应流程(详情见2-2)

  • 中断信号发送到NVIC(嵌套向量中断控制器(Nested Vectored Interrupt Controller))
  • NVIC通知CPU
  • CPU根据中断号得到中断服务程序地址(基地址 + 中断编号 * 4B)
  • 保存现场
  • 执行中断服务程序
  • 恢复现场
  • 继续执行程序
下面以 PendSV_Handler (中断服务程序入口地址)为例,说明一下:
notion image
可以看到PendSV_Handler的中断号是 14,也就是 
而中断向量表的基地址为 0x00000000,故其服务函数的地址在 0x00000038手册中也得到验证。
notion image
  • Bin文件中的地址解析
    • notion image
    • 很多问题啊??????
    • 💡
      Q1:向量表不是应该在 0x00000000 吗?为什么在 bin 文件里却在0x08000000?
      A1:这两个其实是同一块物理内存,只是被“映射”到了两个地址上。也就是说,Flash 同时可以通过两种地址访问:
      • 0x00000000(启动重映射后访问)0x08000000(真实的 Flash 访问地址)
      • 所以 0x080000380x00000038 是访问的同一块 Flash 内存中的内容。
      💡
      Q2:bin文件是什么?
      A2:bin 文件(binary file) 是你编译后的固件镜像,里面是一串二进制数据:
      • 它不包含地址信息
      • 它并不知道地址映射,它按 Flash 的物理地址组织,只是从起始地址开始一段段地填充数据
      • 一般 bin 文件从 0x08000000 开始填充(也就是 STM32 的 Flash 起始地址)
      所以 bin 文件中的第一个字节,就是要烧录到 Flash 的 0x08000000 位置的值。(具体看4 STM32启动流程分析)
      💡
      Q3:(小端存储)解析出来是:0x080002D7,而 map 文件中却是:0x080002D6
      A3:
      原因如下:ARM 指令集有 ARM 指令集和 Thumb 指令集。ARM 指令集位数长;而 Thumb 指令集位数短,故而占用内存比较小,所以编译器大部分时间采用 Thumb 指令集。
      • Thumb 指令集末尾是奇数位 1,像这里的 0x080002D7
      • ARM 指令集末尾是偶数位0,也就是这里的 0x080002D6
      • 而我们访问 0x080002D7 这个地址的时候,它实际上会跳转到 0x080002D6 这个地址。但放入向量表的时候必须+1才能告诉CPU使用的是Thumb模式。(编译器生成中断服务程序入口地址的时候,会自动在最低位加上1)
 
 

3.4 复位程序(Reset Handler

notion image
  • AREA |.text|, CODE, READONLY
    • 定义(申请)一个名称为 .text 的代码段,仅可读。
    • |.text|:这是代码段的名称。| .text | 是一个通用的名称,用来表示存放程序可执行代码的部分。
    • CODE:这是一个属性,表示该区域将包含代码(指令),而不是数据。
    • READONLY:表示该代码段中的内容是只读的,在程序运行过程中不能被修改。
  • Reset handler
    • 系统上电复位之后执行的第一个程序(汇编文件是上电后执行的第一个文件,文件里有很多程序(也可以理解为函数))
  • PROC和ENDP
    • 相当于{},之间的代码相当于函数体
  • EXPORT Reset_Handler [WEAK]
    • EXPORT意为函数在外部可以被引用,也可以在外部被用户定义。
    • [WEAK]弱定义:如果用户没定义,那就用这个汇编文件里面的Reset_Handler函数;如果用户定义了一个新的Reset_Handler,那么就使用用户定义的Reset_Handler函数。
    • 也就是弱定义可以被外部定义覆盖,优先使用外部文件中的定义。外部没有定义,就默认使用启动文件中的。即该函数不是唯一的。
  • IMPORT SystemInitIMPORT __main
    • 表示该标号来自外部文件,跟 C 语言中的 EXTERN关键字类似。引用了外部定义的两个函数。
    • 调用 SystemInit 函数初始化系统时钟
      • SystemInit() 是一个标准的库函数在system_stm32f4xx.c 这个库文件中定义。
    • 调用C库函数__main初始化用户堆栈,并在函数最后调用main()到C程序中去。这就是为什么我们写的程序都有一个 main 函数的原因。
    • LDR:加载一个32位的立即数/地址到指定寄存器(我们的地址就是32位。)
      • 右键可跳转到这个地址对应的函数。SystemInit函数设置微控制器,初始化内嵌的Flash接口、PLL参数,以及更新系统频率。
        • notion image
    • BL:跳转到由寄存器/标号给出的地址,并把跳转前的下一条指针地址保存到LR
    • BLX:执行完返回
      • 跳转到由寄存器给出的地址,并根据寄存器的 LSE 确定处理器的状态,还要把跳转前的下条指令地址保存到 LR
    • BX:执行完不返回 BX:跳转到由寄存器/标号给出的地址,不用返回
notion image
 
 

3.5 中断服务函数

  • 启动文件里面已经帮我们写好所有中断的中断服务函数,跟我们平时写的中断服务函数不一样的就是这些函数都是空的,这里只是提前占了一个位置而已。
  • 真正的中断服务程序需要我们在外部的C文件里面重新实现
notion image
notion image
函数接口弱声明→这些函数都可以在外部重新定义→使用中断函数的时候,必须要重新的定义它,否则程序就会卡死.
下面是一些外设相关的中断服务程序:
  • B.:B 跳转到一个符号。这里跳转到一个“.”,即无限循环。
    • 如果这些接口没有外部定义的话,就会使用当前默认的接口,现在接口都是空的,那么就会无限循环。
  • 一旦发生了异常,内核就需要去响应和处理异常,需要跳转到对应的中断服务程序函数中执行功能,中断服务函数是由用户在外部文件中来重新定义。如果用户开启了某个外设的中断,但是没有编写对应中断服务函数/中断服务函数名字写错了,就会导致程序一直运行汇编文件中定义的中断服务函数,但是预定义的中断服务函数内部是空的,所以就会跳转到无限循环中,导致出不去,程序卡死。
  • 注意:用户在使用外设中断的时候(外部中断,定时器中断,串口中断,看门狗中断…)的时候,中断服务函数的名字必须在汇编文件中查找,不能随意定义。
 
 

3.6 用户堆栈初始化

3.6.1 定义了__MICROLIB

notion image
前面那个叫系统堆栈初始化,用户堆栈初始化由C函数__main来完成
notion image
  • 判断是否定义了宏__MICROLIB:(在keil里面实现)
    • notion image
    • 如果定义了这个宏,则赋予标号 __initial_sp(栈顶地址)、__heap_base(堆起始地址)、__heap_limit(堆结束地址)全局属性,可供外部文件调用。然后堆栈的初始化就由 C 库函数 __main 来完成。
    • 如果启动 MicroLB库,这个库属于KEIL公司自带的库,会替换标准C库,这个微型库会对代码进行高度优化,所以生成的可执行文件的体积会减小,但是缺点是微型库的功能没有标准C库多。
      • 未优化:
        • notion image
      • 优化:
        • notion image
 

3.6.2 未定义__MICROLIB

  • 如果没有定义 __MICROLIB,则才用双段存储器模式,且声明标号 __user_initial_stackheap 具有全局属性,让用户自己来初始化堆栈。
    • notion image
R0:堆空间的首地址
R1:栈空间的首地址+大小(偏移量)→栈顶地址
R2:堆顶地址
R3:栈的首地址
ALIGN 字节对齐,没有指定则默认采用4字节对齐
 
 

4 STM32启动流程分析

4.1 初始化 SP、PC 及中断向量表

  • 当系统复位后,处理器首先读取向量表中的前两个字(8个字节),第一个字存入SP,第二个字存入PC,也就是程序执行的起始地址。
  • 下面打开经过编译生成的 bin 文件,看到前 8 个字节的内容如下(小端模式):
    • notion image
      SP:0X20000660 PC:0X080002D0
  • Keil默认生成hex文件,要想生成bin文件要自己添加命令,设置如下:
    • notion image
      D:\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin --output=Objects/stm32f407.bin Objects/stm32f407.axf
  • 即 SP:0x20000660 和 PC:0x0800020D,下面在 map 文件(存放链接地址)中查找这两个地址存放的是什么数据:
    • notion image
      这正是中断向量表的前两项内容,这也印证了前面所说的内容:
      notion image
 

4.2 设置系统时钟

接下来执行 PC 指向的 Reset_Handler,并调用 SystemInit 初始化系统时钟。
  • 时钟部分参考挑战点2-3,前面的部分是配置时钟的,具体参考手册即可。需要注意的是最后一段代码:默认是没有开启 VECT_TAB_SRAM(即从 SRAM 中启动),表示从 FLASH 中启动,VTOR 寄存器存放的是中断向量表的起始地址,在 IAP 升级会修改这里的偏移量。

4.3 初始化堆栈并进入main

执行完 SystemInit 后又调用了标准库中的 __main 函数:
notion image
notion image
在这里会初始化堆、栈、RO、RW、ZI 段。最后就进入到 C 文件中的 main 函数中。
关于内存部分在下面有详细说。
 

4.4 总结

至此,启动过程圆满结束!
notion image
 
 

5 MDK生成的.map文件简析

5.1 概述

  • .map 文件是连接器生成的一个文件,它主要包含了交叉链接信息。通过 .map 文件,我们可以知道整个工程的函数调用关系、FLASH 和 RAM 占用情况及其详细汇总信息,能具体到单个源文件(.c/.s)的占用情况,根据这些信息,我们可以对代码进行优化。.map 文件可以分为以下 5 个组成部分:
    • 程序段交叉引用关系(Section Cross References
    • 删除映像未使用的程序段(Removing Unused input sections from the image
    • 映射符号表(Image Symbol Table
    • 映射内存分布图(Memory Map of the image
    • 映射组件大小(Image component sizes
    • notion image
我们在 Keil 中最常见的就是在编译之后,编译窗口会显示类似如下一段关于程序和数据大小的信息:
notion image
这一段提示信息其实是汇总了程序和数据的信息,这些信息其实是单个模块汇总而成,在 .map 文件里有详细列表。
.map 文件生成:Project -> Options for Target -> Listing,如下图:
位置:位于Listings文件夹,命名为 项目名.map
notion image
 

5.2 Section Cross References(跨文件引用)

程序段交叉引用/不同文件间的函数调用关系
  • 翻译成中文就是:
    • stm32f4xx_usart模块(stm32f4xx_usart.o)中的USART_DeInit函数调用了stm32f4xx_rcc模块(stm32f4xx_rcc.o)中的RCC_APB1PeriphResetCmd函数
  • i.USART_DeInit 表示 USART_DeInit 函数的入口地址,同理 i.RCC_APB1PeriphResetCmd 表示 RCC_APB1PeriphResetCmd 的入口地址
  • stm32f4xx_usart.o是stm32f4xx_usart.c源文件生成的目标文件模块
notion image
 

5.3 Removing Unused input sections from the image(移除未调用模块)

这部分内容描述了工程中由于未被调用而被删除的冗余程序段(函数/数据)
我们以下面这一行为例:
  • 移除了stm32f4xx_adc模块(stm32f4xx_adc.o)中的ADC_CommonInit函数(占用48字节)
  • 其中该部分最后一行还有所有未调用模块的统计:
notion image
notion image
 

5.4 Image Symbol Table(映像符号表)

映射符号表(Image Symbol Table)描述了被引用的各个符号(程序段/数据)在存储器中的存储地址、类型、大小等信息。
这部分分为两大类 Local Symbols局部 Global Symbols全局
notion image
  • Symbol Name:符号名称 | Value:存储对应的地址 | Ov Type:符号对应的类型 (Number、Section、Thumb Code、Data等) | Size:存储大小 | Object(Section):段目标,即所在模块(源文件)
  • 0x0800xxxx 指存储在 FLASH 里面的代码、变量等。0x2000xxxx 指存储在内存 RAM 中的变量 Data 等

5.4.1 Local Symbols

  • 本地符号(Local Symbols)记录了用static 声明的全局变量地址和大小c 文件中函数的地址和用static 声明的函数代码大小。标号地址(作用域:限本文件)
  • 下面红框处,表示 stm32f4x_dma.c 文件中的 DMA_ClearFlag 函数的入口地址为0x080001ec,类型为:Section(程序段),大小为 0。
    • notion image
 

5.4.2 Global Symbols

  • 全局符号(Global Symbols)记录了全局变量的地址和大小,C 文件中函数的地址及其代码大小,汇编文件中的标号地址(作用域:全工程)
    • notion image
  • 下图中红框框处部分,表示 stm32f4x_dma.c文件中的 DMA_ClearFlag 函数的入口地址为:0x080001ed,类型为:Thumb Code,(程序段),大小为 38 字节。
    • notion image
    • 注意,此处的地址用的0x080001ed,和上面 的 0x080001ec 地址不符,这是因为 ARM 规定 Thumb 指令集的所有指令(Thumb 指令集更节省空间),其最低位必须为 1(偶数加 1),0x080001ed = 0x080001ec + 1,所以才会有 2 个不同的地址,且总是差 1,实际上就是同一个函数。
💡
一个小知识点:字节对齐是什么?
  • 字节对齐是指程序中 数据(变量)在内存中的存储位置。为了提高访问效率,计算机系统通常要求数据的起始地址满足某些特定的条件,比如数据的类型要求在内存中以某个特定的地址对齐。
notion image
  • 一个例子:定义一个结构体example含有char a和int b两个变量
notion image
 

5.5 Memory Map of the image(映像内存分布)

  • 映像文件:映像文件通常是指一个包含代码、数据和其他必要信息的 二进制文件,编译和链接过程生成的,可以被下载到硬件(比如微控制器、嵌入式系统等)上进行运行。
    • 格式:.bin.hex.elf
    • 💡
      奇奇怪怪的区分:映像(Image) VS 映射(Mapping)
      • 映像(Image)是指某个程序的完整 二进制文件;
        • 映像文件被加载到内存中并运行时,我们称之为 程序映像,它就是程序运行所需要的完整内容。
      • 映射(Mapping)是将程序或数据从某个存储位置加载到内存中的一个地址
        • 定义存储位置内存位置之间的关系
        • 映射入口地址是指程序的入口地址,也就是程序开始执行的位置。
  • 映像文件分为加载域(Load Region)运行域(Execution Region)
    • 加载域:程序实际存储区域(物理存储地址)
    • 运行域:程序实际执行时的内存区域
    • 一个加载域必须有至少一个运行域(可以有多个运行域),而一个程序又可以有多个加载域(即程序的不同部分可以存储在不同的物理位置)。
      • notion image
 
notion image
  • Image Entry point : 指映射入口地址,如果自己重构了start文件,会出现Not specified(不影响正常进入程序,但是会WARNING,可以在魔术棒的Linker下的Misc controls 处加入入口地址:--entry Reset_Handler)
    • 下面是没有重写的Image Entry point
      • notion image
        Image Entry point : 0x08000195 表示映射入口地址。
  • Load Region LR_IROM1
    • Load Region LR_IROM1 (Base: 0x08000000, Size: 0x0000464, Max: 0x00080000, ABSOLUTE)表示加载区域位于 LR_IROM1, 开始地址0x08000000,大小有 0x0000464,这块区域最大为 0x00080000
  • Execution Region:执行区域
    • Execution Region ER_IROM1:在内部 FLASH 运行域,所有需要放内部 FLASH 的代码(code,data),都应该放到这个运行域里面
    • Execution Region RW_IRAM1:在内部 SRAM 运行域,所有RAM(包括 RW 和 ZI)都是放在这个运行域里面
    • Type:类型(有Data,Code,Zero,PAD等) | Attr:属性 | Section Name:段名 | Object:目标
 

5.6 Image component sizes(映像组件大小)

给出了整个映像所有代码(.o)占用空间的汇总信息,对我们比较有用,如图所示:
notion image
  • Debug:表示调试数据所占的空间大小,如调试输入节及符号和字符串。
  • Object Totals:表示以上部分链接到一起后,所占映像空间的大小。
  • (incl.Generated):表示链接器生产的映像内容大小,它包含在 Object Totals 里面了,这里仅仅是单独列出,我们一般不需要关心。
  • (incl.Padding):表示链接器根据需要插入填充以保证字节对齐的数据所占空间的大小,它也包含在 Object Totals 里面了,这里单独列出,一般无需关心。
这里表示被提取的库成员(.lib 添加到映像中的部分所占空间大小。各项意义同前面提到的说明。我们一般只用看 Library Totals 来分析库所占空间的大小即可。
notion image
  • Grand Totals:表示整个映像所占空间大小。
  • LF Image Totals:表示 ELF 可执行链接格式映像文件的大小,一般和 Grand Totals 一样大小。
  • ROM Totals:表示整个映像所需要的 ROM 空间大小,不含 ZI 和 Debug 数据。
 

6 MDK生成的.sct文件简析

  • .sct 文件(或叫做 scatter-loading file)是 Keil MDK中用于描述程序内存布局的一个配置文件,它的作用是指定程序各个部分在内存中如何存放。(Linux对应的是.ld文件)
  • 链接器根据该文件的配置分配各个节区地址,生成分散加载代码,因此我们通过修改该文件可以定制具体节区的存储位置。
  • sct 文件中主要包含描述加载域及执行域的部分,一个文件中可包含有多个加载域,而一个加载域可由多个部分的执行域组成。同等级的域之间使用花括号"{}“分隔开,最外层的是加载域,第二层”{}"内的是执行域,其整体结构见下图。
notion image
notion image
下面是对代码的解释:
notion image
我们不妨试一下修改中断向量表的位置 首先关闭Use Memory Layout From Target Dialog,代表用我们自己的.sct来加载
notion image
更改.sct文件,把RESET放入RAM中
notion image
Rebuild后再打开.map文件
可以看到RESET已经被我们拷贝到了RAM中了
notion image
 

7 一些补充内容

7.1 单片机和X86 CPU 运行程序的不同

X86的PC机CPU在运行的时候程序是存储在RAM中的,单片机等嵌入式系统程序则是存于Flash中。 
  • X86CPU和单片机读取程序的具体途径
    • pc机在运行程序的时候将程序从外存(硬盘)中,调入到RAM中运行,cpu从RAM中读取程序和数据而单片机的程序则是固化在flash中,cpu运行时直接从flash中读取程序,从RAM中读取数据。
    • x86构架的cpu是基于冯.诺依曼体系的,即数据和程序存储在一起,而且pc机的RAM资源相当丰富,从几十M到几百M甚至是几个G,客观上能够承受大量的程序数据。
    • 单片机的构架大多是哈弗体系的,即程序和数据分开存储,而且单片的片内RAM资源是相当有限的,内部的RAM过大会带来成本的大幅度提高。
  • 冯.诺依曼体系与哈佛体系的区别
    • 二者的区别就是程序空间和数据空间是否是一体的。
    • 早期的微处理器大多采用冯诺依曼结构,典型代表是Intel公司的X86微处理器。取指令和取操作数都在同一总线上,通过分时复用的方式进行的。缺点是在高速运行时,不能达到同时取指令和取操作数,从而形成了传输过程的瓶颈。
    • 哈佛总线技术应用是以DSP和ARM为代表的。采用哈佛总线体系结构的芯片内部程序空间和数据空间是分开的,这就允许同时取指令和取操作数,从而大大提高了运算能力。

7.2 ARM 和 x86cpu 指令集RISC和CISC说明

  • ARM芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:
    • 对内存只有读、写指令
    • 对于数据的运算是在CPU内部实现
    • 使用RISC指令的CPU复杂度小一点,易于设计
  • x86属于复杂指令集计算机(CISC:Complex Instruction Set Computing),它所用的指令比较复杂,比如某些复杂的指令,它是通过“微程序”来实现的。

7.3 进制换算

我们都知道数据单位有:bit、byte、word、KB、MB、GB、TB等等,他们之间的换算很简单,例如:
  • 0x400转换的十进制为:1024,,也就是有 1024 个字节(Byte),即1KB
  • 地址:0x4000 0000 - 0x4000 0FFF
    • 内存大小 = 结束地址 - 起始地址(转换成十进制后) + 1
    • 单位是Bytes(字节):每个字节都有一个独立的地址(内存地址),程序访问内存时,实际上是通过访问字节地址来读取和写入数据。
  • Flash Memory (0x0800 0000 - 0x0807 FFFF):512KB 0x80000
  • SRAM (0x2000 0000 - 0x2001 7FFF):96KB 0x18000
 

8 重写start文件(了解即可)

notion image
notion image
notion image
notion image
 
 

9 参考博客

再谈应用程序分段: 数据段、代码段、BSS段以及堆和栈:
linux 目标文件的组成,逆向分析 ELF 文件结构
嵌入式进制间的换算:
2-1 用Keil实现μC/OS-II工程搭建Chapter1:Introduction
Loading...
🐟🐟
🐟🐟
在坚冰还盖着北海的时候,我看到了怒放的梅花
最新发布
2-1 用Keil实现μC/OS-II工程搭建
2025-5-10
Chapter1:Introduction
2025-5-7
2-4 启动ARM STM32
2025-5-6
1-4-1 USART串口协议
2025-5-6
1-1-2 GPIO小记
2025-5-6
1-1-1 用Keil点亮一盏小灯
2025-5-6
公告
🎉NotionNext 3.15已上线🎉
-- 感谢您的支持 ---
👏欢迎更新体验👏