1. 硬件基础与地址空间

1.1 MCU 组成与存储特性

单片机简化模型为若干部件经总线连接:

部件作用掉电后
CPU 内核(Cortex-M4)取指、译码、执行——
Flash存放代码与常量内容保留
SRAM存放运行时变量、栈、堆内容丢失
外设(GPIO/SPI/USART 等)与外部交互配置丢失
总线(AHB/APB)连接上述部件——

1.2 Cortex-M 4GB 地址空间

ARM 将各部件统一编址到 4GB 空间,整体布局由内核规范约定,各厂家在区域内填充实际容量:

起始地址区域说明
0x0000_0000Code 区(0.5GB)内部 Flash 典型映射在 0x0800_00000x00 起始一段为别名区,映射目标由启动配置决定
0x2000_0000SRAM 区(0.5GB)内部 SRAM 所在
0x4000_0000外设区(0.5GB)GPIO、SPI 等外设寄存器
0xE000_0000内核私有外设总线NVIC、SysTick、DWT 等

1.3 核心寄存器

  • PC(R15,程序计数器):指向下一条待执行指令的地址,程序运行的本质是 PC 持续移动。
  • SP(R13,栈指针):指向栈顶。存在两个物理分身:MSP(主栈指针)PSP(进程栈指针),由 CONTROL 寄存器的 SPSEL 位选择生效者。裸机程序通常全程使用 MSP;PSP 一般留给 RTOS 为各任务提供独立栈,MSP 则服务于中断与内核。
  • LR(R14,链接寄存器):保存函数返回地址,复位后值为 0xFFFF_FFFF
  • R0~R12 为通用寄存器。
  • 复位后默认使用 MSP,运行在特权级(可访问全部寄存器);与之相对的非特权级用于 OS 隔离用户任务。

1.4 栈的生长方式

Cortex-M 采用满递减栈:压栈时 SP 先减 4 再写入数据,栈向低地址方向生长。因此栈顶初值需设置在 SRAM 顶端(确切说为末端地址再加 1,首次压栈前会先自减)。

2. 复位与启动源选择

2.1 上电复位与复位源

电源接通后电压爬升到工作阈值需要时间,期间数字电路状态不确定,芯片内部复位电路持续拉住复位信号,直至电源稳定才释放。

  • POR(Power-On Reset,上电复位):内部电源电压检测电路,电压低于阈值保持复位,升高并稳定后释放。
  • NRST 引脚:外部复位脚,拉低触发同等复位,复位按键即接于此。
  • 复位源:触发复位的原因统称,包括上电、外部引脚、看门狗、软件复位(NVIC 的 SYSRESETREQ)等。复位/时钟控制模块(AT32 称 CRM,STM32 称 RCC)中通常有寄存器可读出本次复位源。

2.2 别名区与地址重映射

内部 Flash 位于 0x0800_0000,而内核硬件规定复位后从 0x00 取向量。芯片通过地址重映射(remap),根据 BOOT 引脚上电电平,将 0x00 起始的**别名区(Aliased region)**映射到不同物理存储器:

典型启动源0x00 映射到用途
主 Flash0x0800_0000正常运行程序
系统存储器出厂固化 ROM内含厂家 bootloader,用于串口/USB 等方式烧录(ISP)
内部 SRAM0x2000_0000调试用途,较少使用

因此复位后访问的 0x00 是别名区,相当于 Flash 开头的一个窗口;烧录在 0x0800_0000 的第一个字,复位时即可从 0x0000_0000 读到。BOOT 引脚数量与启动源组合因型号而异,需以具体芯片参考手册为准。

2.3 复位释放后的硬件动作

复位信号释放瞬间,内核硬件自动完成两步操作,此时尚未执行任何程序代码:

  1. 0x0000_0000 读出 4 字节写入 MSP(栈顶地址);
  2. 0x0000_0004 读出 4 字节写入 PC(复位向量,即 Reset_Handler 地址)。

随后 CPU 从 PC 指向处开始取指执行。先初始化栈指针的原因:任何函数调用与任何异常处理都依赖栈,栈必须先于其他一切就绪。

3. 中断向量表与异常机制

3.1 向量表结构

0x0000_0000(Flash 开头)存放的不是指令,而是中断向量表(Vector Table)——一张每项 4 字节的地址数组:

偏移内容
0x00栈顶地址(MSP 初值,非函数)
0x04Reset_Handler 地址(复位向量)
0x08~0x3C内核异常(NMI、HardFault、SVC、PendSV、SysTick 等,共 16 项)
0x40外部中断(USART1、SPI1、定时器等,每外设一项)

3.2 异常与中断相关概念

  • 异常(Exception):打断正常程序流、迫使 CPU 跳转执行处理函数的事件统称。复位属于异常,其优先级高于其他所有异常。
  • 中断(Interrupt):由外设触发的异常,对应向量表第 16 项起的表项。
  • ISR(中断服务程序):向量表项指向的处理函数,如 USART1_IRQHandler
  • NVIC(嵌套向量中断控制器):内核标配的中断管理器,负责中断使能、优先级仲裁与嵌套。中断发生后,NVIC 按中断号直接查向量表取得处理函数地址并跳转,无需软件逐个查询中断来源。

3.3 异常进入与自动压栈

异常发生时,CPU 自动将 xPSR、PC、LR、R12、R3~R0 共 8 个字压入当前栈(保护现场),随后查表跳转至 ISR;ISR 末尾通过特殊返回指令(EXC_RETURN)触发硬件自动出栈,恢复被中断的现场。该机制同样解释了栈指针须在复位后第一时间初始化的原因。

3.4 VTOR

VTOR(Vector Table Offset Register,向量表偏移寄存器) 记录向量表当前所在位置。复位后默认值为 0(指向别名区),之后可将向量表迁至其他地址(如 bootloader 跳转 app、程序重定位场景)。Cortex-M4 要求 VTOR 基址按向量表大小向上取整到 2 的幂对齐,下限为 128 字节。

4. 启动文件与时钟初始化

4.1 Reset_Handler

Reset_Handler 与整张向量表位于启动文件(Keil 工程通常为 startup_xxx.s,CMSIS 标准组成部分,由芯片厂家提供)。向量表在汇编中以 DCD 指令逐项列出:

1
2
3
4
5
6
__Vectors   DCD     _initial_sp              ; 栈顶地址
            DCD     Reset_Handler            ; 复位处理
            DCD     NMI_Handler
            DCD     HardFault_Handler
            ...
            DCD     USART1_IRQHandler        ; 外部中断从此开始

Reset_Handler 的典型内容(Keil/ARMCC 工具链)为两步:

1
2
3
4
5
6
7
Reset_Handler   PROC
                EXPORT  Reset_Handler   [WEAK]
                LDR     R0, =SystemInit
                BLX     R0
                LDR     R0, =__main
                BX      R0
                ENDP

即先调用 SystemInit,再进入 __main

4.2 弱符号机制

启动文件中所有中断处理函数均以 [WEAK](弱符号)给出默认实现(通常为死循环)。链接时若工程中存在同名函数则采用自定义版本,否则采用默认版本。由此只需实现实际使用的中断,不必写齐全部 ISR。

4.3 SystemInit

SystemInit 位于 system_xxx.c(厂家提供),主要完成:

  1. 配置系统时钟:复位后芯片默认由内部 RC 振荡器(HSI,精度一般)直接驱动;SystemInit 一般负责切换至外部晶振(HSE)并启动 PLL(锁相环) 倍频,将主频提升至目标值。相关概念为时钟树(Clock Tree):振荡源 → PLL 倍频 → 分频器 → 内核及各外设的完整分配网络。部分厂家将倍频放在 main() 内的时钟配置函数中,目的相同。
  2. 设置 VTOR(经 VECT_TAB_OFFSET 宏)。
  3. 使能 FPU(浮点运算单元)(Cortex-M4F,写 CPACR 寄存器);未使能时执行浮点指令会触发硬件错误。

5. C 运行时环境的建立

5.1 直接跳转 main() 的问题

__main 并非应用代码中的 main(),而是 Keil ARMCC 运行时库入口(GCC 工具链对应 _start 或启动汇编中手写的拷贝循环)。以下代码说明了运行时环境尚未就绪的问题:

1
2
3
4
5
6
7
int g_count = 10;   /* .data:初值 10 从哪来? */
int g_flag;         /* .bss:谁来保证开机是 0? */

int main(void)
{
    while (g_flag == 0) { g_count++; }
}

CPU 刚复位时 SRAM 中为随机值。g_count 等于 10、g_flag 等于 0 属于 C 语言的语义保证,兑现该保证的正是 __main

5.2 段的划分

编译链接后,链接器按属性将内容分为若干段(section):

内容烧录后存放运行位置
.text机器码FlashFlash 原地执行
.rodata常量、const 数据FlashFlash
.data初值非 0 的全局/静态变量初值存 FlashSRAM
.bss未初始化或初值为 0 的变量不占 FlashSRAM
heap(堆)malloc 动态分配——SRAM,向上生长
stack(栈)局部变量、调用现场——SRAM,自顶端向下生长

5.3 .data 拷贝与 .bss 清零

.data 变量本体须在 SRAM 中方可读写,而其初值为常量、烧录时只能存于 Flash,因此 .data 具有两个地址:

  • 加载地址(LMA):初值在 Flash 中的存放位置;
  • 运行地址(VMA):变量在 SRAM 中的工作位置。

__main 的核心工作:

  1. 拷贝:将 .data 初值从 Flash(LMA)整体搬至 SRAM(VMA);
  2. 清零:将 .bss 对应 SRAM 区域全部写 0;
  3. 完成后调用应用代码的 main()

堆与栈无需初始化内容,链接时规划边界即可:栈自 SRAM 顶端向下生长,堆自 .bss 末尾向上生长,中间间隔耗尽即为栈溢出,可能破坏堆或全局变量,表现为异常死机。

5.4 链接脚本与分散加载文件

各段的地址归属由链接描述文件规定:

  • Keil ARMCC:分散加载文件(scatter file,.sct,以加载域(Load Region,烧录位置)与执行域(Execution Region,运行位置)描述 LMA/VMA 关系;
  • GCC:链接脚本(linker script,.ldMEMORY 定义 Flash/SRAM 起始与大小,SECTIONS 定义各段归属。

划分规则可概括为:需修改的内容(变量、栈、堆)必须进入 SRAM,只读内容(代码、常量)留在 Flash 以节省 RAM。启动文件引用的 _initial_sp 同样由链接脚本定义,取值为 SRAM 末端。

5.5 XIP

XIP(eXecute In Place) 指代码在 Flash 中原地执行,无需先搬入 RAM,是内部 Flash 单片机的常规执行方式。

6. 烧录与存储布局

6.1 烧录仅写入 Flash

烧录(ISP、J-Link/SWD 烧写)只写非易失存储器,即 Flash。SRAM 为易失介质,且烧录时目标程序并未运行,烧录器不会将运行时 SRAM 内容作为固件写入。

6.2 烧录文件的内容

hex/bin 烧录文件的地址记录全部落在 Flash 区间:

内容是否存在于烧录文件烧写目标
向量表 .isr_vector存在Flash
.text存在Flash
.rodata存在Flash
.data 初值副本存在,按 LMA(Flash 地址)存放Flash
.data 变量本体不存在运行时由启动代码建立于 SRAM
.bss不存在(NOBITS,仅占大小信息)运行时清零于 SRAM

axf/elf 文件额外携带符号与段描述(LMA、VMA、大小),供调试器与 fromelf 等工具使用,烧录动作本身仍只落 Flash。

6.3 加载域视图与执行域视图

烧录文件对应加载域视图:描述哪些字节位于 Flash 的哪个位置。运行时对应执行域视图.data 本体与 .bss 位于 SRAM。两种视图之间的搬运由启动代码(__main)在每次上电后完成,SRAM 布局并非烧录产物,而是上电后按链接脚本规划现场重建的。

6.4 烧写过程中的 SRAM 使用与 RAM 运行例外

  • 烧写算法:J-Link 等烧录器工作时,会先向芯片 SRAM 下载一段自带的烧写算法程序(J-Link 称 RAMCode,Keil 中对应 Flash Algorithm 文件),由 CPU 执行该代码操作 Flash 控制器完成擦写。该程序属于烧录工具的临时组件,与目标固件的 .data.bss 无关。
  • RAM 运行:调试阶段可将链接脚本改为全部链到 SRAM,由调试器写入镜像后运行(断电即失,属于调试手段而非烧录);部分芯片(如 STM32H7)将关键代码搬至 ITCM 加速,搬运同样由启动代码完成。

6.5 布局验证方法

  1. 查阅 Keil 生成的 .map 文件:各 Execution Region 与 Load Region 的地址、大小均有列出,可直接看到 .data 的 Execution Base 位于 SRAM、Load Base 位于 Flash;
  2. 使用 fromelf --text -c 或 objdump 反汇编 axf,查看各段 LMA/VMA;
  3. 直接查看 hex 文件(Intel HEX 为文本格式):所有数据记录地址均在 0x0800_xxxx 范围,不存在 0x2000_xxxx 的记录,可作为烧录仅写 Flash 的直接证据。

7. main() 之后的运行形态与 bootloader

7.1 裸机与 RTOS 形态

main() 中的典型流程为:外设初始化(GPIO、SPI、USART、DMA、中断优先级),随后:

  • 裸机:进入 while(1) 主循环与中断配合的架构。main() 不应返回,返回将落入运行时库 exit() 的死循环。
  • RTOS:创建任务后启动调度器,PSP 投入使用为各任务提供独立栈,SysTick 中断驱动任务切换,MSP 专职服务中断。

7.2 bootloader 的跳转机制

bootloader 本质上是一个普通程序,职责是在跳转 app 之前,手动重演一遍硬件复位时内核完成的动作。带 bootloader 时 Flash 布局为:bootloader 占据头部(0x0800_0000 起),app 位于其后,芯片复位后总是先执行 bootloader,由其决定进入升级流程或跳转 app。

跳转的简化实现:

1
2
3
4
5
6
jump_addr  = *(uint32_t *)(APP_ADDR + 4);          /* app 的 Reset_Handler */
jump_stack = *(uint32_t *)(APP_ADDR + 0);          /* app 的栈顶           */

__set_MSP(jump_stack);                              /* ① 重设栈顶          */
SCB->VTOR = APP_ADDR;                               /* ② 向量表指向 app     */
((void (*)(void))jump_addr)();                      /* ③ 跳转,不再返回     */

跳转前通常还需关闭全局中断、将已用外设与时钟恢复至复位状态,避免 bootloader 残余配置污染 app。app 侧需在链接脚本中将起始地址改为 APP_ADDR,并保证 SystemInit 中 VTOR 偏移一致。

7.3 ISP、IAP 与双分区 OTA

概念说明典型进入方式/场景
ISP使用芯片出厂 ROM 中固化的 bootloader 烧录程序BOOT 引脚选择系统存储器启动
IAP使用自编写于 Flash 的 bootloader 升级 app任意通信接口(串口、以太网等),适用于远程固件升级
双分区 OTAFlash 存放 A/B 两份 app,bootloader 校验后选择跳转升级失败可回滚

8. 启动流程总览

  1. 上电:POR 电路拉住复位,等待电源稳定;
  2. 复位释放:硬件读 0x00 → MSP,读 0x04 → PC(Reset_Handler);
  3. SystemInit:配置时钟树(HSI/HSE/PLL)、使能 FPU、设置 VTOR;
  4. __main.data 初值自 Flash 拷至 SRAM,.bss 区域清零;
  5. main():外设初始化,进入 while(1) 主循环或 RTOS 调度器;
  6. 运行期间中断随时插入:NVIC 查向量表 → 自动压栈 → ISR → 自动出栈返回。

附录 A 术语速查

名词说明
PC / SP / LR程序计数器 / 栈指针 / 链接寄存器(返回地址)
MSP / PSP主栈指针(复位后及中断使用)/ 进程栈指针(RTOS 任务使用)
特权级 / 非特权级复位后默认为特权级,可访问全部寄存器;非特权级用于 OS 隔离
别名区 / 重映射0x00 起始的可配置窗口,由 BOOT 引脚决定映射目标
BOOT 引脚上电时选择启动源的引脚,组合方式见参考手册
向量表Flash 开头的地址表:栈顶 + 各异常/中断处理函数入口
异常 / 中断 / ISR打断程序流的事件 / 外设触发的异常 / 对应处理函数
NVIC内核中断控制器:使能、优先级仲裁、查表跳转
VTOR记录向量表当前位置的寄存器
启动文件厂家提供的汇编文件:向量表 + Reset_Handler + 默认 ISR
[WEAK] 弱符号可被同名自定义函数覆盖的默认实现
SystemInit厂家提供的时钟/FPU/VTOR 初始化函数
时钟树 / HSI / HSE / PLL时钟分配网络 / 内部 RC / 外部晶振 / 锁相环倍频器
FPU硬件浮点单元,需使能后方可执行浮点指令
.text / .rodata / .data / .bss代码 / 常量 / 已初始化变量 / 未初始化变量
LMA / VMA段的存储地址 / 运行地址(.data 两者不同,需拷贝)
XIP代码在 Flash 中原地执行
链接脚本 / 分散加载文件规定各段地址划分的文件(GCC 的 .ld / Keil 的 .sct
满递减栈压栈先减后存、向低地址生长的栈
bootloader / ISP / IAP引导并跳转 app 的程序 / 出厂 ROM 烧录 / 自编 bootloader 升级
POR / NRST / 复位源上电复位电路 / 外部复位引脚 / 复位原因统称
RAMCode烧录器下载到 SRAM 执行的自带烧写算法

说明:机制性内容(取 MSP/PC、向量表结构、压栈行为、段拷贝)属于 Cortex-M 内核规范,适用于 AT32F415;BOOT 引脚组合、Flash/SRAM 容量、时钟树具体频率等与型号强绑定,需以对应料号的数据手册与参考手册为准。