1. 硬件基础与地址空间
1.1 MCU 组成与存储特性
单片机简化模型为若干部件经总线连接:
| 部件 | 作用 | 掉电后 |
|---|---|---|
| CPU 内核(Cortex-M4) | 取指、译码、执行 | —— |
| Flash | 存放代码与常量 | 内容保留 |
| SRAM | 存放运行时变量、栈、堆 | 内容丢失 |
| 外设(GPIO/SPI/USART 等) | 与外部交互 | 配置丢失 |
| 总线(AHB/APB) | 连接上述部件 | —— |
1.2 Cortex-M 4GB 地址空间
ARM 将各部件统一编址到 4GB 空间,整体布局由内核规范约定,各厂家在区域内填充实际容量:
| 起始地址 | 区域 | 说明 |
|---|---|---|
0x0000_0000 | Code 区(0.5GB) | 内部 Flash 典型映射在 0x0800_0000;0x00 起始一段为别名区,映射目标由启动配置决定 |
0x2000_0000 | SRAM 区(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 映射到 | 用途 |
|---|---|---|
| 主 Flash | 0x0800_0000 | 正常运行程序 |
| 系统存储器 | 出厂固化 ROM | 内含厂家 bootloader,用于串口/USB 等方式烧录(ISP) |
| 内部 SRAM | 0x2000_0000 | 调试用途,较少使用 |
因此复位后访问的 0x00 是别名区,相当于 Flash 开头的一个窗口;烧录在 0x0800_0000 的第一个字,复位时即可从 0x0000_0000 读到。BOOT 引脚数量与启动源组合因型号而异,需以具体芯片参考手册为准。
2.3 复位释放后的硬件动作
复位信号释放瞬间,内核硬件自动完成两步操作,此时尚未执行任何程序代码:
- 从
0x0000_0000读出 4 字节写入 MSP(栈顶地址); - 从
0x0000_0004读出 4 字节写入 PC(复位向量,即Reset_Handler地址)。
随后 CPU 从 PC 指向处开始取指执行。先初始化栈指针的原因:任何函数调用与任何异常处理都依赖栈,栈必须先于其他一切就绪。
3. 中断向量表与异常机制
3.1 向量表结构
0x0000_0000(Flash 开头)存放的不是指令,而是中断向量表(Vector Table)——一张每项 4 字节的地址数组:
| 偏移 | 内容 |
|---|---|
0x00 | 栈顶地址(MSP 初值,非函数) |
0x04 | Reset_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 指令逐项列出:
| |
Reset_Handler 的典型内容(Keil/ARMCC 工具链)为两步:
| |
即先调用 SystemInit,再进入 __main。
4.2 弱符号机制
启动文件中所有中断处理函数均以 [WEAK](弱符号)给出默认实现(通常为死循环)。链接时若工程中存在同名函数则采用自定义版本,否则采用默认版本。由此只需实现实际使用的中断,不必写齐全部 ISR。
4.3 SystemInit
SystemInit 位于 system_xxx.c(厂家提供),主要完成:
- 配置系统时钟:复位后芯片默认由内部 RC 振荡器(HSI,精度一般)直接驱动;
SystemInit一般负责切换至外部晶振(HSE)并启动 PLL(锁相环) 倍频,将主频提升至目标值。相关概念为时钟树(Clock Tree):振荡源 → PLL 倍频 → 分频器 → 内核及各外设的完整分配网络。部分厂家将倍频放在main()内的时钟配置函数中,目的相同。 - 设置 VTOR(经
VECT_TAB_OFFSET宏)。 - 使能 FPU(浮点运算单元)(Cortex-M4F,写 CPACR 寄存器);未使能时执行浮点指令会触发硬件错误。
5. C 运行时环境的建立
5.1 直接跳转 main() 的问题
__main 并非应用代码中的 main(),而是 Keil ARMCC 运行时库入口(GCC 工具链对应 _start 或启动汇编中手写的拷贝循环)。以下代码说明了运行时环境尚未就绪的问题:
| |
CPU 刚复位时 SRAM 中为随机值。g_count 等于 10、g_flag 等于 0 属于 C 语言的语义保证,兑现该保证的正是 __main。
5.2 段的划分
编译链接后,链接器按属性将内容分为若干段(section):
| 段 | 内容 | 烧录后存放 | 运行位置 |
|---|---|---|---|
.text | 机器码 | Flash | Flash 原地执行 |
.rodata | 常量、const 数据 | Flash | Flash |
.data | 初值非 0 的全局/静态变量 | 初值存 Flash | SRAM |
.bss | 未初始化或初值为 0 的变量 | 不占 Flash | SRAM |
| heap(堆) | malloc 动态分配 | —— | SRAM,向上生长 |
| stack(栈) | 局部变量、调用现场 | —— | SRAM,自顶端向下生长 |
5.3 .data 拷贝与 .bss 清零
.data 变量本体须在 SRAM 中方可读写,而其初值为常量、烧录时只能存于 Flash,因此 .data 具有两个地址:
- 加载地址(LMA):初值在 Flash 中的存放位置;
- 运行地址(VMA):变量在 SRAM 中的工作位置。
__main 的核心工作:
- 拷贝:将
.data初值从 Flash(LMA)整体搬至 SRAM(VMA); - 清零:将
.bss对应 SRAM 区域全部写 0; - 完成后调用应用代码的
main()。
堆与栈无需初始化内容,链接时规划边界即可:栈自 SRAM 顶端向下生长,堆自 .bss 末尾向上生长,中间间隔耗尽即为栈溢出,可能破坏堆或全局变量,表现为异常死机。
5.4 链接脚本与分散加载文件
各段的地址归属由链接描述文件规定:
- Keil ARMCC:分散加载文件(scatter file,
.sct),以加载域(Load Region,烧录位置)与执行域(Execution Region,运行位置)描述 LMA/VMA 关系; - GCC:链接脚本(linker script,
.ld),MEMORY定义 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 布局验证方法
- 查阅 Keil 生成的
.map文件:各 Execution Region 与 Load Region 的地址、大小均有列出,可直接看到.data的 Execution Base 位于 SRAM、Load Base 位于 Flash; - 使用
fromelf --text -c或 objdump 反汇编 axf,查看各段 LMA/VMA; - 直接查看 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。
跳转的简化实现:
| |
跳转前通常还需关闭全局中断、将已用外设与时钟恢复至复位状态,避免 bootloader 残余配置污染 app。app 侧需在链接脚本中将起始地址改为 APP_ADDR,并保证 SystemInit 中 VTOR 偏移一致。
7.3 ISP、IAP 与双分区 OTA
| 概念 | 说明 | 典型进入方式/场景 |
|---|---|---|
| ISP | 使用芯片出厂 ROM 中固化的 bootloader 烧录程序 | BOOT 引脚选择系统存储器启动 |
| IAP | 使用自编写于 Flash 的 bootloader 升级 app | 任意通信接口(串口、以太网等),适用于远程固件升级 |
| 双分区 OTA | Flash 存放 A/B 两份 app,bootloader 校验后选择跳转 | 升级失败可回滚 |
8. 启动流程总览
- 上电:POR 电路拉住复位,等待电源稳定;
- 复位释放:硬件读
0x00→ MSP,读0x04→ PC(Reset_Handler); - SystemInit:配置时钟树(HSI/HSE/PLL)、使能 FPU、设置 VTOR;
- __main:
.data初值自 Flash 拷至 SRAM,.bss区域清零; - main():外设初始化,进入
while(1)主循环或 RTOS 调度器; - 运行期间中断随时插入: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 容量、时钟树具体频率等与型号强绑定,需以对应料号的数据手册与参考手册为准。