← 返回嵌入式学习资料第 3 章 · Cortex-M 与启动流程

CHAPTER 3 · CORTEX-M STARTUP

Cortex-M 与启动流程
从复位到 main

从寄存器、栈和执行模式开始,沿着复位、向量表、启动代码、内存映射和异常处理一路追踪;把 ELF 的静态地址与目标机真正启动的现场证据明确分开。

6 个一级单元52 个递进小节主机路径可复现

TEXTBOOK MAP

CHAPTER 3 · MAP

从复位入口到可复查启动

六个一级单元对应六种系统认知:理解内核、读懂向量、解释 runtime、建立地址地图、保存故障现场、完成目标机交付。

先理解处理器正在做什么
  • 01 Cortex-M 执行模型
  • 02 复位与向量表
再把启动链逐段拆开
  • 03 Reset_Handler 与 C runtime
  • 04 Flash、SRAM 与外设映射
最后留下故障与交付证据
  • 05 异常、故障与恢复
  • 06 ELF 到目标机启动

小节数量按认知任务变化:复位、runtime、故障与交付需要更多递进步骤;每个单元都保留无目标机主机路径。

使用方式:先从左侧选择一个启动认知,再按小节中的概念、机制、证据和练习推进。每个单元底部有前后翻页;小节标题直接说明正在追踪的内核状态、地址或启动动作。
主机边界:默认使用标准库模型、C 源文件和工具输出阅读;没有目标板也能完成向量表、启动顺序、地址地图和故障记录。
目标机边界:真实复位、时钟、Flash/RAM、异常、供电和行为必须回到当前芯片、板卡、启动文件、链接脚本和工具链的官方资料。

01 · CORTEX-M CORE

Cortex-M 执行模型:寄存器、栈与指令边界

Cortex-M 不是一块抽象的‘单片机 CPU’。模式、寄存器、Thumb 指令、栈指针、内存访问和异常规则共同决定 C 代码如何执行。本单元从可观察的寄存器和调用边界出发,为后面的复位与启动建立共同语言。

看懂读懂 Cortex-M 的基本执行状态。
说清把 C 调用映射到寄存器和栈。
开始做为反汇编和目标机观察留下证据。
官方基线Cortex-M0+ Devices Generic User Guide处理器寄存器、异常、栈与调试语义的 Arm 官方基线。锁定:DUI 0662B · 核对日期:2026-09-05

3.1.1 从线程模式看程序正在做什么

Cortex-M 的线程模式通常承载应用代码,处理模式承载异常和中断处理;特权级又决定某些系统控制寄存器是否可访问。具体芯片是否实现某些可选特性要看内核版本和配置。描述一段代码时,模式、特权、当前栈和异常嵌套都应成为条件,而不是默认背景。

3.1.2 寄存器组:名字背后的调用合同

R0-R3 常用于传递前几个参数和返回值,R4-R11 等寄存器可能由被调用者保存,LR 保存返回线索,PC 指向下一条取指位置,xPSR 保存条件和状态。这里是 ABI 层面的工作模型,不是鼓励读者凭编号猜测每个编译器的具体指令;最终判断要回到目标 ABI 与反汇编。

C · REGISTER SNAPSHOT
#include <stdint.h> typedef struct { uint32_t r0, r1, r2, r3; uint32_t r12, lr, pc, xpsr; } exception_frame_t; static int valid_thumb_entry(uint32_t address) { return (address & 1u) != 0u; }

这是主机可阅读的寄存器/入口模型;真实寄存器值和入口合法范围仍需目标内核与 ELF/现场证据。

C · STACK BOUNDARY
#include <stddef.h> static int within_stack(uintptr_t sp, uintptr_t low, uintptr_t high, size_t frame) { return sp >= low && sp <= high && frame <= (size_t)(sp - low); }

模型只检查数值边界,不会测量真实栈水位、异常嵌套或 MPU 属性。

3.1.3 栈指针:MSP 与 PSP 不是两个随便的变量

MSP 常用于异常处理和早期启动,PSP 可以被操作系统或应用线程使用。复位后只有初始 MSP 来自向量表的第一个字,PSP 是否配置、何时切换以及是否存在 RTOS,都属于工程选择。把‘SP’写进记录时,要注明当前使用的是哪个栈和哪个时刻。

表 1 · Cortex-M 执行模型:寄存器、栈与指令边界的证据判断表
对象应留下的证据不能直接推出
模式线程/处理、特权所有代码都在同一模式
寄存器ABI、栈、返回PC/LR 可随意当数据
证据反汇编、快照、目标一段 C 代码证明硬件

3.1.4 Thumb 与取指:地址最低位的含义

在 Cortex-M 上,Thumb 状态是执行指令的基本约定,向量表中的处理函数地址通常还带有表示指令状态的最低位约束。读者不应把这个最低位当作普通代码地址的一部分去随意清零或设置;应由芯片、工具链和启动文件共同解释。

3.1.5 调用帧:C 函数如何保存现场

一个函数的栈帧可能保存返回地址、被调用者保存寄存器、局部对象和对齐填充。优化等级会改变保存数量,内联可能让函数没有独立帧,异常又会额外压入硬件上下文。栈分析必须固定编译选项和调用路径,不能只看源代码中的局部变量数量。

图 1.1 · Cortex-M 执行模型:寄存器、栈与指令边界的判断路径每一格都要有自己的条件。
C 代码函数、局部对象
ABI寄存器与栈
指令Thumb 与访存
观察反汇编与快照

从左到右是思考顺序,不是把四类证据合并成一个“通过”。

3.1.6 内存访问:指针不是自动安全的总线请求

指针运算只负责产生数值地址,不负责确认地址所在区域可访问。Cortex-M 的 Flash、SRAM、外设和系统控制空间可能有不同权限、等待状态、对齐和副作用;一次读取外设寄存器还可能清除标志。把指针当作普通整数使用,会把硬件边界藏起来。

3.1.7 反汇编证据:可以证明什么

反汇编能证明某次构建产生了哪些指令、使用了哪些寄存器以及某个符号位于何处;它不能证明复位脚、电源、时钟稳定、总线故障或中断输入真的发生。保存反汇编时必须附上 ELF、目标、编译选项和源版本,否则地址和指令缺少上下文。

3.1.8 本节交付:一张 Cortex-M 执行卡

执行卡的最小字段包括当前模式、栈指针来源、函数入口、调用者/被调用者责任、可能的异常和证据文件。它把‘CPU 在运行’拆成几个可回答的问题,后续阅读向量表与 Reset_Handler 时就不会把第一条指令和应用行为混为一谈。

CHECKPOINT 3.1把“C 函数调用怎样变成 Cortex-M 上的寄存器、栈和指令?”写成一份可复查记录

请围绕“Cortex-M 执行模型:寄存器、栈与指令边界”写出对象、输入或复位条件、状态/机制、输出、一个约束、一个失败路径和对应证据。没有目标板时,运行主机路径并把未执行字段标为未执行;拥有目标机时,也要补齐芯片、板卡、供电、工具链、启动文件、链接脚本和观察条件。不要用一次下载成功或一个寄存器截图覆盖没有做过的验证。

合格记录要让没有现场上下文的读者知道先查什么、先运行什么、看到什么才算通过,以及哪一句结论仍然不能推出。未知项和停止条件不是扣分项,而是交付的一部分。

02 · RESET AND VECTOR TABLE

复位与向量表:程序从哪里开始

复位不是一种单一事件,向量表也不是一张普通函数表。上电、外部复位、看门狗、软件复位和调试复位可能留下不同状态;向量表首字提供初始栈,后续字提供异常入口,VTOR、启动模式和芯片映射共同决定 CPU 实际读取的位置。

看懂区分复位来源与复位后状态。
说清读懂向量表、VTOR 和入口地址。
开始做把静态地址证据与现场启动现象分开。
官方基线CMSIS Device G0启动文件、设备头文件、向量名与芯片定义的固定来源。锁定:v1.4.5 · 核对日期:2026-09-05

3.2.1 复位来源:同一个入口不代表同一个上下文

上电复位、欠压复位、外部 NRST、看门狗复位和软件触发的系统复位可能有不同的原因标志、外设保留状态和时钟状态。芯片数据手册会规定哪些寄存器在何种复位后清零或保留。不要把‘重新下载后能进 main’等同于所有复位来源都已经验证。

3.2.2 向量表布局:第一个字和第二个字

向量表的第一个字通常提供初始主栈指针,第二个字提供复位处理函数入口,后续位置对应异常和中断。它的具体长度、保留项、对齐要求和存储映射随内核与芯片而变;C 数组示例只能帮助读者阅读,不能代替启动文件和参考手册。

C · VECTOR TABLE SHAPE
#include <stdint.h> typedef void (*handler_t)(void); extern uint32_t __stack_top; void Reset_Handler(void); void Default_Handler(void); __attribute__((section(".isr_vector"))) const uintptr_t vectors[] = { (uintptr_t)&__stack_top, (uintptr_t)Reset_Handler, (uintptr_t)Default_Handler, };

向量节名、地址、对齐和映射必须与具体芯片启动文件、链接脚本和复位映射核对。

C · VTOR CHECK
#include <stdint.h> static int vector_base_ok(uintptr_t base, uintptr_t flash_start, uintptr_t flash_end, uintptr_t alignment) { return base >= flash_start && base < flash_end && (base % alignment) == 0u; }

它只检查教学模型的范围和对齐;不能证明目标机已经执行了 VTOR 写入或从新表取向量。

3.2.3 VTOR:向量表可以搬到哪里

VTOR 允许在支持的 Cortex-M 配置中选择向量表位置,但地址对齐、可执行存储区、写入时机和安全状态都有限制。搬移向量表前,必须确认新区域在复位后可访问、不会被启动清零覆盖,并在需要时设置内存屏障和同步操作。

表 2 · 复位与向量表:程序从哪里开始的证据判断表
对象应留下的证据不能直接推出
复位来源、原因、状态所有复位完全相同
向量MSP、入口、对齐符号存在就已启动
VTOR地址、时机、映射任意地址都可搬移

3.2.4 异常号与默认处理器

弱符号让工程可以提供 Default_Handler,但也可能让错误的处理函数名字静默回退。一个默认死循环被触发时,先记录异常号、堆栈帧和向量表地址,再检查强符号、链接脚本和中断使能;不要只把默认处理器改成‘继续运行’来隐藏问题。

3.2.5 启动模式与存储映射

启动模式可能决定芯片从用户 Flash、系统存储器或 SRAM 取第一段代码,调试器下载又可能改变复位和 halt 行为。板卡跳帽、BOOT 脚、映射别名和工具参数应作为对象条件记录;同一份 ELF 在不同启动模式下不一定有相同入口。

图 2.1 · 复位与向量表:程序从哪里开始的判断路径每一格都要有自己的条件。
事件上电/看门狗/软件
映射VTOR 与存储区
向量初始栈/入口
入口Reset_Handler

从左到右是思考顺序,不是把四类证据合并成一个“通过”。

3.2.6 入口地址:指令状态、对齐和合法范围

入口地址必须同时满足地址范围、执行属性、对齐和指令状态要求。链接器输出一个数字不代表处理器接受它;错误的向量字可能立即触发 HardFault,也可能跳进一个看似可执行但没有正确初始化的区域。检查应同时使用 ELF、脚本和现场读数。

3.2.7 复位后寄存器:不要把残留值当初始化值

复位后寄存器的‘默认值’只在指定复位类型和供电条件下成立。启动代码可能很早就写入时钟、看门狗或内存保护寄存器,调试器还可能预先配置设备。记录寄存器时写清楚是在复位保持、入口前、main 前还是运行中读取的。

3.2.8 调试复位:暂停改变了观察条件

调试器的 halt-on-reset 便于读取现场,却会改变时钟、看门狗和超时的时间关系。一次在断点下看到的寄存器快照不能直接代表无人连接调试器时的启动。应至少保留一次自由运行或受控日志的证据,并记录调试设置。

3.2.9 本节交付:一张复位到入口的证据图

复位到入口的证据图要把静态和动态材料并列:链接脚本/ELF 说明处理器应该取哪里,复位原因和寄存器说明它从哪种状态来,日志或 GPIO 现象说明是否真正走过入口。三者不能由一个截图互相替代。

CHECKPOINT 3.2把“复位事件怎样通过向量表找到正确的入口?”写成一份可复查记录

请围绕“复位与向量表:程序从哪里开始”写出对象、输入或复位条件、状态/机制、输出、一个约束、一个失败路径和对应证据。没有目标板时,运行主机路径并把未执行字段标为未执行;拥有目标机时,也要补齐芯片、板卡、供电、工具链、启动文件、链接脚本和观察条件。不要用一次下载成功或一个寄存器截图覆盖没有做过的验证。

合格记录要让没有现场上下文的读者知道先查什么、先运行什么、看到什么才算通过,以及哪一句结论仍然不能推出。未知项和停止条件不是扣分项,而是交付的一部分。

03 · STARTUP RUNTIME

启动代码与 C runtime:从 Reset_Handler 到 main

Reset_Handler 不是一个神秘黑盒。它要建立栈、搬运已初始化数据、清零 bss、设置系统时钟和必要的核心状态,随后才把控制权交给 C runtime 和 main。不同厂商启动文件会加入不同动作,但每一个动作都应该能被命令、符号或现场现象复核。

看懂按顺序解释启动初始化动作。
说清区分 C runtime 与芯片系统初始化。
开始做为启动失败保留可定位证据。
官方基线CMSIS Device G0启动文件、设备头文件、向量名与芯片定义的固定来源。锁定:v1.4.5 · 核对日期:2026-09-05

3.3.1 Reset_Handler 的最小顺序

最小 Reset_Handler 通常先加载栈指针,再执行数据初始化,最后跳到 C 入口;真实工程还会加入时钟、FPU、内存保护、看门狗和厂商 SystemInit。顺序不是所有芯片完全相同,读者应以当前启动文件和链接脚本为准,并把额外动作标出来。

3.3.2 加载地址与运行地址:data 为什么要搬家

.data 的初始值常位于 Flash 的加载地址,运行时地址在 RAM;启动代码根据链接符号把一段字节复制过去。如果加载地址、运行地址或长度有一个不一致,程序可能在 main 前就读取错误值。map 和启动汇编应一起查看,不能只看 C 声明。

C · SECTION INITIALIZATION
#include <stddef.h> #include <stdint.h> static void copy_words(uint32_t *dst, const uint32_t *src, size_t words) { for (size_t i = 0; i < words; ++i) dst[i] = src[i]; } static void clear_words(uint32_t *dst, size_t words) { for (size_t i = 0; i < words; ++i) dst[i] = 0u; }

示例展示动作顺序的可测试部分;真实边界来自链接符号和启动文件,不能用固定数组代替。

C · STARTUP ORDER
typedef void (*startup_step_t)(void); void Reset_Handler(void) { set_initial_stack(); copy_data_from_flash(); clear_bss(); SystemInit(); runtime_init(); main(); }

函数名是教学占位符;目标工程应以当前 CMSIS/厂商启动实现为准,并为每步保存证据。

3.3.3 bss 清零:从‘默认零’到真实动作

.bss 没有逐字节的 Flash 初值,启动代码只需要按链接符号给它清零。若启动代码跳过、范围算错或在清零前调用 C,静态全局对象会带着不确定内容进入应用。主机进程的零初始化习惯不能替代裸机启动证据。

表 3 · 启动代码与 C runtime:从 Reset_Handler 到 main的证据判断表
对象应留下的证据不能直接推出
dataFlash 初值→RAMC 声明自动搬运
bss启动清零范围主机零初始化替代裸机
时钟来源、锁定、超时写寄存器等于稳定

3.3.4 SystemInit:时钟与核心状态的交接

SystemInit 可能设置外部晶振、PLL、分频、Flash 等待状态和核心时钟变量,但是否成功需要状态位、超时和测量支撑。把函数返回或寄存器写入当成‘时钟已稳定’过于宽泛;应记录锁定条件、失败分支和实际时钟来源。

3.3.5 FPU、缓存与内存屏障:可选能力要有前置

FPU、缓存和内存屏障是能力与时序的组合。编译器选了硬浮点 ABI,目标核心却没有按工程方式启用 FPU,可能在调用边界或第一条浮点指令处失败。启用缓存或改变映射后,也可能需要失效、清理和屏障。每个可选能力都要有版本和顺序证据。

图 3.1 · 启动代码与 C runtime:从 Reset_Handler 到 main的判断路径每一格都要有自己的条件。
复位MSP 与入口
初始化data/bss/时钟
运行时库与构造器
应用main

从左到右是思考顺序,不是把四类证据合并成一个“通过”。

3.3.6 C runtime 与构造器:main 之前还有什么

C runtime 可能调用初始化数组、除法辅助函数、锁、堆或半主机入口。裸机工程应明确哪些库特性被保留、哪些被裁剪,以及它们依赖的系统调用如何实现。看到 main 被调用,不代表所有库对象或静态构造器都按预期执行。

3.3.7 启动阶段的最小依赖

启动阶段的好依赖是少而明确:栈、只读常量、已知的核心寄存器和链接符号。串口、动态内存、复杂日志和外部中断往往还没有准备好。若为了诊断加入这些依赖,要把它们的前置条件和失败时的替代信号写清楚。

3.3.8 启动失败的定位顺序

定位启动失败时不应一次性重写整个启动文件。先确认向量入口和 MSP,再检查 data/bss 边界,随后单步观察 SystemInit 和 main 前的标志。每一步都保留最后已知正常的 ELF、寄存器、时间和复位来源,避免把问题扩大成新的变量组合。

3.3.9 本节交付:启动时序与证据表

启动时序表最好有‘动作、输入、输出、失败、证据、未验证’六列。它能让读者区分静态地址、启动代码动作和现场现象,也能在后续 GPIO、UART 或 RTOS 章节继续复用,而不是每一章重新发明一套报告格式。

CHECKPOINT 3.3把“谁负责把一片刚复位的内存变成可以运行 C 的世界?”写成一份可复查记录

请围绕“启动代码与 C runtime:从 Reset_Handler 到 main”写出对象、输入或复位条件、状态/机制、输出、一个约束、一个失败路径和对应证据。没有目标板时,运行主机路径并把未执行字段标为未执行;拥有目标机时,也要补齐芯片、板卡、供电、工具链、启动文件、链接脚本和观察条件。不要用一次下载成功或一个寄存器截图覆盖没有做过的验证。

合格记录要让没有现场上下文的读者知道先查什么、先运行什么、看到什么才算通过,以及哪一句结论仍然不能推出。未知项和停止条件不是扣分项,而是交付的一部分。

04 · MEMORY MAP AND SYSTEM CONTROL

内存映射:Flash、SRAM 与外设窗口

Cortex-M 的地址空间把代码、数据、外设和系统控制寄存器放在统一的可寻址世界里,但统一寻址不等于统一语义。Flash 可能只读且有等待状态,SRAM 需要预算,外设读写可能有副作用,系统控制空间又有特权和屏障要求。

看懂从芯片手册建立内存地图。
说清把地址、属性和副作用写进证据。
开始做连接链接脚本、C 指针和实际总线访问。
官方基线RM0444 STM32G0x1 reference manual寄存器、复位值、时序和外设边界以本参考手册修订版为准。锁定:Rev 6 · 核对日期:2026-09-05

3.4.1 统一地址空间不等于统一存储

Cortex-M 的统一地址空间让 C 指针可以指向 Flash、SRAM、外设和系统控制区,但每个区域的访问规则由芯片和内核定义。地址本身没有告诉你是否可写、是否可执行、是否需要对齐或是否会触发副作用;这些字段必须从资料和工程配置补齐。

3.4.2 Flash:执行、读取与擦写边界

Flash 的执行和读取常受等待状态、缓存、擦写粒度和电源条件影响;在线擦写时某些区域可能不能继续取指。把常量放在 Flash 与把 Flash 当作运行时数据库是不同设计,后者还需要擦写寿命、掉电一致性和恢复合同。

LD · TARGET MEMORY MAP
MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 32K } SECTIONS { .text : { *(.isr_vector) *(.text*) *(.rodata*) } > FLASH .data : { *(.data*) } > SRAM AT > FLASH .bss : { *(.bss*) *(COMMON) } > SRAM }

仅用于阅读 MEMORY/SECTIONS;地址、长度、属性和启动符号必须替换为具体 MCU 的官方数据。

C · REGISTER ACCESS CONTRACT
#include <stdint.h> #define REG32(address) (*(volatile uint32_t *)(address)) static void set_bits(uintptr_t address, uint32_t mask) { REG32(address) |= mask; }

寄存器地址、宽度、保留位、屏障和副作用不能从宏本身推出;驱动必须绑定芯片参考手册。

3.4.3 SRAM:数据、栈和保留空间

SRAM 预算应包含启动复制后的 .data、.bss、堆、最深调用栈、中断嵌套、DMA 缓冲区和保留区。一个 map 文件只能说明静态映像,运行时水位还要结合填充标记、监控或最坏路径分析。剩余数字不是‘可以随便使用的内存’。

表 4 · 内存映射:Flash、SRAM 与外设窗口的证据判断表
对象应留下的证据不能直接推出
Flash执行、常量、擦写地址就是可写权限
SRAM数据、栈、DMA、预算剩余空间无限可用
外设宽度、掩码、副作用像普通 RAM 复制

3.4.4 外设窗口:读写可能有副作用

外设寄存器的读写语义可能包括读清除、写一清零、写保护、保留位和必须按顺序访问。把寄存器映射成普通结构体并连续复制,可能读掉状态或写入保留位。驱动应使用手册规定的宽度、掩码、屏障和时序,并留下版本依据。

3.4.5 系统控制空间:SCB、NVIC 与屏障

SCB、NVIC 和系统控制寄存器常要求特权访问,并可能在写入后使用 DSB/ISB 等屏障确保后续取指和访存看到新状态。屏障不是万能同步原语,也不能替代外设握手;它解决的是架构可见性和执行顺序中的一部分问题。

图 4.1 · 内存映射:Flash、SRAM 与外设窗口的判断路径每一格都要有自己的条件。
代码Flash/执行
数据SRAM/栈
设备寄存器窗口
系统SCB/MPU

从左到右是思考顺序,不是把四类证据合并成一个“通过”。

3.4.6 MPU 与属性:权限也是行为合同

MPU、缓存和共享属性改变了访问的权限与可见性,但是否存在、粒度多大、复位默认值是什么,取决于具体 Cortex-M 配置。工程记录应分开写内核能力、芯片实现、启动配置和应用意图;主机上通过的指针测试不能证明目标属性。

3.4.7 地址证据:手册、脚本、ELF、现场

地址证据有四个层次:参考手册说明物理区域,链接脚本说明工程放置,ELF/map 说明本次构建结果,现场读数/故障状态说明一次访问发生了什么。它们冲突时不要挑一个看似合理的数字,而要记录对象、版本和最小复现。

3.4.8 本节交付:一份目标内存地图

目标内存地图应带区域名称、起止、长度、读写执行属性、启动动作、所有者、预算和证据链接。这样下一章增加 GPIO 寄存器或 DMA 缓冲区时,读者可以看到新增对象改变了哪个区域,以及哪些属性仍未验证。

CHECKPOINT 3.4把“Cortex-M 如何把代码、数据和外设放进同一个地址空间?”写成一份可复查记录

请围绕“内存映射:Flash、SRAM 与外设窗口”写出对象、输入或复位条件、状态/机制、输出、一个约束、一个失败路径和对应证据。没有目标板时,运行主机路径并把未执行字段标为未执行;拥有目标机时,也要补齐芯片、板卡、供电、工具链、启动文件、链接脚本和观察条件。不要用一次下载成功或一个寄存器截图覆盖没有做过的验证。

合格记录要让没有现场上下文的读者知道先查什么、先运行什么、看到什么才算通过,以及哪一句结论仍然不能推出。未知项和停止条件不是扣分项,而是交付的一部分。

05 · EXCEPTIONS AND FAULTS

异常与故障:让启动失败可定位

异常是 Cortex-M 的控制流,不是程序之外的噪声。异常号、优先级、自动压栈帧、SCB 故障状态、向量表和处理函数共同决定一次故障能留下什么。好的故障处理不会立即吞掉现场,而是保存最小上下文,再决定复位、停机或恢复。

看懂区分异常入口与普通函数。
说清读取自动压栈帧和故障状态。
开始做设计可恢复、可复盘的故障路径。
官方基线Cortex-M0+ Devices Generic User Guide处理器寄存器、异常、栈与调试语义的 Arm 官方基线。锁定:DUI 0662B · 核对日期:2026-09-05

3.5.1 异常入口:不是普通函数调用

异常进入会切换到处理模式,并根据当前架构配置选择栈、压入硬件上下文、读取向量并跳转到处理函数;普通 BL 调用则由 ABI 约定保存返回线索。两者都可能进入 C,但返回方式、可用栈和禁止动作不同,不能把异常处理器当成普通业务函数。

3.5.2 自动压栈帧:故障时 CPU 留下什么

自动压栈通常包含 R0-R3、R12、LR、返回 PC 和 xPSR,若使用浮点上下文或发生嵌套,帧可能扩大或延后保存。读取帧前要判断使用 MSP/PSP、基本帧/扩展帧和栈地址是否有效,否则诊断代码本身可能再次故障。

C · FAULT SNAPSHOT
#include <stdint.h> typedef struct { uint32_t r0, r1, r2, r3, r12, lr, pc, xpsr; } fault_frame_t; volatile fault_frame_t last_fault; volatile uint32_t fault_cfsr; void save_fault(const fault_frame_t *frame, uint32_t cfsr) { last_fault = *frame; fault_cfsr = cfsr; }

保存结构应放入已确认的保留区域;此片段不替代真实异常入口的汇编取帧和复位策略。

C · FAULT CLASSIFICATION
static const char *fault_kind(uint32_t cfsr, uint32_t hfsr) { if (cfsr & 0x000000FFu) return "memory-or-bus"; if (cfsr & 0x0000FF00u) return "usage"; if (hfsr != 0u) return "hardfault-escalation"; return "unknown"; }

位定义必须按当前 Cortex-M 版本核对;模型只练习先保存原始状态再分类。

3.5.3 CFSR/HFSR:状态位要和条件一起读

CFSR 由多个子状态组成,HFSR 可以表示某些故障升级,MMFAR/BFAR 只有在对应有效位成立时才可解释。读寄存器时要保存原始值、复位原因和时间;先清零再读取会毁掉最有价值的证据。

表 5 · 异常与故障:让启动失败可定位的证据判断表
对象应留下的证据不能直接推出
现场帧、CFSR/HFSR、原因先清寄存器再分析
处理最小保存与指示复杂日志一定安全
恢复复位、停机、行为看门狗重启就是修复

3.5.4 HardFault:最后入口不是最后原因

HardFault 是入口,不一定是根因。一个不可执行地址、总线错误、未定义指令或被屏蔽的可配置故障都可能最终到这里。应先看 CFSR/HFSR、堆栈帧、向量表和最近一次映像,再决定是代码、地址、时序、供电还是工具链问题。

3.5.5 NVIC 优先级与嵌套

NVIC 优先级数值的解释受优先级位数和分组配置影响;高优先级抢占会增加栈和时序压力,尾链可能减少部分切换开销。记录中应写出配置函数、实际实现位数、屏蔽寄存器和嵌套路径,而不是只写‘优先级设为 2’。

图 5.1 · 异常与故障:让启动失败可定位的判断路径每一格都要有自己的条件。
触发异常/中断
保存自动压栈
诊断SCB 状态
动作停机/复位/恢复

从左到右是思考顺序,不是把四类证据合并成一个“通过”。

3.5.6 SysTick 与系统节拍

SysTick 是一个异常源,不是自动正确的系统时钟。它依赖重载值、核心时钟、使能时机和处理函数;如果在时钟切换前启动或重载值溢出,延时和超时都会失去意义。启动记录需说明 SysTick 何时开始以及用什么信号校准。

3.5.7 故障处理器:最小动作与安全动作

故障处理器应尽量只做确定、有限、可重入的动作:保存寄存器到保留 RAM,设置一个简单故障指示,必要时触发受控复位。复杂 printf、动态分配、依赖尚未启动的串口或再次访问可疑外设,都会让原始故障被二次故障覆盖。

3.5.8 看门狗与复位闭环

看门狗复位可以把设备从死循环拉回启动路径,却不会自动告诉你死循环发生在哪里。要在复位前留下原因码、最后心跳、固件版本和计数器,并在下一次启动读取它们。只有看到合同行为恢复,才能把它称为恢复而不是重复重启。

3.5.9 本节交付:一份故障复盘记录

故障复盘记录应把‘事实’和‘推断’分栏。事实包括原始寄存器和日志,推断包括可能的非法访问或栈溢出,动作包括停止、复位和下一次只改的变量。这样即使根因尚未确定,下一位读者也不会从一个猜测继续扩大变量。

CHECKPOINT 3.5把“HardFault、NMI 和中断发生时,怎样保存足够证据而不是只看到死循环?”写成一份可复查记录

请围绕“异常与故障:让启动失败可定位”写出对象、输入或复位条件、状态/机制、输出、一个约束、一个失败路径和对应证据。没有目标板时,运行主机路径并把未执行字段标为未执行;拥有目标机时,也要补齐芯片、板卡、供电、工具链、启动文件、链接脚本和观察条件。不要用一次下载成功或一个寄存器截图覆盖没有做过的验证。

合格记录要让没有现场上下文的读者知道先查什么、先运行什么、看到什么才算通过,以及哪一句结论仍然不能推出。未知项和停止条件不是扣分项,而是交付的一部分。

06 · STARTUP DELIVERY

启动交付:从 ELF 到可复查的目标机证据

第 3 章的交付不是‘板子亮了’,而是一份能从干净目录复核的启动证据包:读者能找到目标、工具链、启动文件、链接脚本、ELF/map、复位原因、向量表和故障记录,也能知道没有目标板时哪些部分可以先完成。

看懂建立启动证据包和版本清单。
说清分离静态地址、启动动作和现场行为。
开始做把故障与恢复记录交给下一章。
官方基线STM32CubeG0课程设备支持、启动代码与示例的锁定来源;链接指向固定 tag。锁定:v1.6.3 · 核对日期:2026-09-05

3.6.1 证据包最小集合

启动证据包至少需要能回答‘谁构建了它、把什么放在哪里、复位后做了什么、现场看到了什么、还没有验证什么’。源码和 bin 只能回答其中一部分;启动文件、链接脚本、ELF/map、复位日志和资料版本要互相引用。

3.6.2 工具链与目标身份

目标身份要写到芯片型号、封装/修订、板卡型号、调试器和工具链目标三元组。‘Cortex-M4’不是完整目标身份,不同厂商的内存、外设、FPU 配置和启动映射都可能不同。身份字段缺失时,地址和时序结论应自动降级为模型或待核对。

PYTHON · STARTUP MANIFEST
from pathlib import Path import hashlib root = Path("build") for path in sorted(root.glob("*")): if path.is_file(): print(path.name, path.stat().st_size, hashlib.sha256(path.read_bytes()).hexdigest())

脚本只清点构建产物;它不能证明目标机复位、时钟、向量表或合同行为。

SHELL · EVIDENCE COMMANDS
arm-none-eabi-size build/firmware.elf arm-none-eabi-nm -S --size-sort build/firmware.elf > build/firmware.nm.txt arm-none-eabi-objdump -h -t build/firmware.elf > build/firmware.sections.txt

目标工具链未安装时继续完成主机实验包;命令的目标、版本和输出文件必须写入记录。

3.6.3 向量表与 ELF 交叉核对

向量表交叉核对可以从三处开始:ELF 中的 .isr_vector 节和符号,链接脚本对该节的放置,目标机在复位后对应地址的内存窗口。三者一致仍只能说明地址关系;还要通过复位、入口或受控 GPIO 现象证明处理器真的走过这条路径。

表 6 · 启动交付:从 ELF 到可复查的目标机证据的证据判断表
对象应留下的证据不能直接推出
静态脚本、ELF、map已经证明现场行为
启动时序、寄存器、日志一个断点足够
交付版本、限制、恢复只交付 bin

3.6.4 启动时序记录

启动时序记录要标出时间参考。复位释放、读取向量、进入 Reset_Handler、完成 data/bss、配置时钟、进入 main 和第一个可见行为之间可能有不同延迟。没有时间窗的‘启动成功’无法与看门狗、调试 halt 或电源爬升条件比较。

3.6.5 主机路径:没有板子先完成什么

没有板子时可以完成对象合同、阶段化构建、脚本阅读、向量表模型、故障分类和记录模板;不能声称已测量真实复位、时钟、Flash/RAM、GPIO 或中断。把未执行写明,读者仍然有一条完整且诚实的学习路径。

图 6.1 · 启动交付:从 ELF 到可复查的目标机证据的判断路径每一格都要有自己的条件。
来源手册/脚本/版本
构建ELF/map
启动向量/时序
交付现象/限制

从左到右是思考顺序,不是把四类证据合并成一个“通过”。

3.6.6 目标机路径:安全前置与停止

目标机前置条件包括指定 USB/低压供电、已核对板卡手册、正确调试器、无未知外部接线和可恢复的固件路径。接线、地址或复位原因不确定时停止;不要用反复上电替代诊断。安全边界是启动证据的一部分。

3.6.7 单变量启动回归

单变量回归可以改变一个链接脚本区域、一个启动选项、一个初始化动作或一个工具链版本,但不能同时换板、换脚本和换代码。先写预测,再比较 ELF/map/日志/现象,最后写出解释和限制;差异不可解释时保留失败现场。

3.6.8 资料卡:官方事实与教学抽象

资料卡应区分内核通用事实、CMSIS 接口、厂商启动实现、芯片参考手册和板卡用户手册。Arm/CMSIS 资料能帮助理解接口与异常,不能替代具体芯片的复位寄存器和存储映射。每条引用都要写它支撑的句子与不支撑的句子。

3.6.9 本章结业:交给 GPIO 章节的接口

交给 GPIO 章节的接口包括:main 前哪些系统已经准备好,栈和内存如何验证,异常如何保存,时钟与 SysTick 是否有证据,以及目标机仍有哪些未知。这样下一章讨论寄存器和引脚时,不会重新假设启动已经成立。

CHECKPOINT 3.6把“怎样把启动文件、链接脚本、工具版本和现场记录交给下一位读者?”写成一份可复查记录

请围绕“启动交付:从 ELF 到可复查的目标机证据”写出对象、输入或复位条件、状态/机制、输出、一个约束、一个失败路径和对应证据。没有目标板时,运行主机路径并把未执行字段标为未执行;拥有目标机时,也要补齐芯片、板卡、供电、工具链、启动文件、链接脚本和观察条件。不要用一次下载成功或一个寄存器截图覆盖没有做过的验证。

合格记录要让没有现场上下文的读者知道先查什么、先运行什么、看到什么才算通过,以及哪一句结论仍然不能推出。未知项和停止条件不是扣分项,而是交付的一部分。

资料卡(访问日期:2026-09-03):CMSIS-Core、Startup、NVIC 和 SysTick 文档支撑内核接口与启动组织;ST UM3256 支撑具体 Nucleo 板卡对象核对;Arm CMSIS 官方 GitHub 仓库支撑源码与版本追溯。本站的“主机/目标机边界”是教学解释,具体芯片、复位寄存器、地址和启动文件仍须按读者当前版本核对。