STM32 程序从源码到运行:编译、烧录与启动全流程详解
1. 整体认识
我们写STM32程序的完整链路本质上是一条翻译+搬运+启动的流水线:
你在编辑器里敲下的人类可读C代码 → 经过工具链一步步翻译成CPU能识别的二进制机器码 → 通过调试器硬件把二进制写到芯片内部的非易失Flash里 → 芯片上电复位后,硬件自动从Flash里取指令,把程序跑起来。
整个流程可以拆成三个核心阶段:
- 编译阶段:把C源码转成可烧录的镜像文件,解决“CPU看不懂人话”的问题
- 烧录阶段:把生成的镜像通过硬件接口写到芯片内部Flash,解决“程序怎么进芯片”的问题
- 启动阶段:芯片上电后自动加载程序到运行状态,解决“芯片怎么从头跑起来”的问题
平时我们点一下Keil里的“Download”按钮,背后就是整套流程在毫秒级跑完。
搞懂每个环节的细节,才能在出现“程序烧不进去”“调试点不准”“跑飞HardFault”的时候快速定位问题。
很多人觉得这些是“底层原理,会调库就行”——等你遇到上面说的那些诡异bug的时候就知道:
只知道HAL库函数怎么调用,和知道整个链路怎么跑起来,排查问题的速度差10倍。
2. 机器语言与指令速通
2.1 什么是“机器语言”
机器语言就是CPU能直接识别、不需要任何翻译的二进制电信号序列。
没有任何“语法”“关键字”,就是一串高低电平组成的0和1,存在Flash里,CPU的总线接口读到这些位模式后,译码电路会直接对应到固定的硬件动作。
比如你在Keil反汇编窗口看到的:
0x08000000 0x20000100 0x08000004 0x08000121这两行就是纯机器码——前者是栈顶初始值,后者是复位中断的入口地址,CPU根本不知道什么是
int、什么是函数,它只认这些二进制位的排列组合。2.2 认识“指令”
一条指令就是CPU执行一个最小原子操作的编码单元。
一条指令对应一个固定的硬件动作,比如:
- 把寄存器R0的值加到R1上
- 从内存地址0x20000000读一个32位数到R2
- 跳转到0x08000200地址继续执行
- 触发一次中断
所有CPU的复杂功能,拆到最后都是成百上千条基础指令按顺序执行出来的。
哪怕是你写的一个1ms的延时函数,拆到底也就是几百条加减、跳转、空操作指令在循环跑。
2.3 指令集
一个CPU硬件设计时规定好的、能识别的所有指令的集合,就是指令集。
STM32F103用的是Cortex-M3内核,属于ARMv7-M架构,支持Thumb-2指令集:
- 传统ARM指令是固定32位长度,代码密度低,占空间大
- 纯Thumb指令是16位长度,代码密度高,但指令数少,功能受限
- Thumb-2是混合指令集:16位和32位指令可以无缝混用,既保留了高代码密度,又补全了32位指令的功能,是Cortex-M内核的标配
关键细节:
Cortex-M内核没有传统ARM状态,全程运行Thumb指令。
这也是为什么STM32的中断向量表里,所有中断入口地址的最低位永远是1——这个1就是告诉CPU“接下来要执行的是Thumb指令”,如果是0会直接触发硬件错误。
2.4 指令执行过程
CPU执行一条指令的标准流程是经典的三级流水线:
- 取指:程序计数器PC指向下一条指令的Flash地址,CPU通过内部总线把指令从Flash读到指令缓存
- 译码:指令译码电路把读到的二进制位翻译成对应的控制信号,告诉ALU、寄存器堆、总线控制器要做什么操作
- 执行:硬件按控制信号完成运算,更新寄存器或内存的值,同时PC指向下一条指令
流水线的核心价值是并行执行:
在执行第N条指令的同时,CPU已经在译码第N-1条、取第N-2条指令,不用等一条指令完全跑完再开始下一条,吞吐量比单周期执行提升了近3倍。
Cortex-M3没有复杂的分支预测(单片机程序逻辑简单,没必要),三级流水线是完全固定的。
补充细节:
Cortex-M3的中断响应延迟是固定的12个周期——硬件自动把xPSR、PC、LR、R0-R3、R12这8个寄存器压进栈,然后从向量表读中断入口地址跳过去,全程硬件完成,不需要软件存现场,这也是STM32中断响应快的原因。
3. STM32 程序的编译
3.1 认识“高级语言”
我们写的C/C++就是高级语言——它是给人读的,用接近自然语言的抽象(变量、循环、函数、结构体)描述逻辑,CPU完全不认识。
从C代码到机器码,必须经过编译器的全流程翻译。
3.2 认识“编译器”
编译器就是做“翻译”的工具链,它的任务是把人类写的C代码,等价转换成目标CPU能跑的机器码,同时还要做优化,让代码更小、跑的更快。
STM32开发最常用的编译器就是ARM官方的armclang(Keil5默认内置,是老armcc的下一代版本,基于LLVM架构)。
3.3 认识“armclang”
armclang是ARM专门为Cortex-M/R/A系列内核定制的LLVM编译器:
- 对Cortex-M3/M4/M7做了深度指令级优化,生成的代码比老armcc体积小10%~20%,执行速度更快
- 完整支持C99/C11/C++11等现代语言标准
- 自带完善的编译警告检查,能帮你提前发现很多隐蔽bug
- 和Keil、STM32CubeIDE完全集成,不需要手动配环境
3.4 重要的编译选项
编译选项直接决定了最终生成代码的大小、速度和可调试性,嵌入式开发最常用的几个:
选项 作用 -O0零优化,调试模式专用:所有变量都存在内存里,不会被优化掉,断点不会“乱跑”,但是代码体积大、跑的慢 -Os优化代码体积,嵌入式产品发布默认选项:在不影响性能的前提下把代码压到最小,适合Flash紧张的场景 -O2最高性能优化:适合跑算法、对速度要求高的场景,但是调试困难,变量会被优化没 -g生成调试符号:把“C代码行号 ↔ 内存地址”的对应关系存到axf文件里,没有这个选项调试器根本不知道你的断点打在哪个地址 -mcpu=cortex-m3指定目标内核:编译器只会生成Cortex-M3支持的指令,不会生成其他内核不认识的指令 -Wall打开所有常规警告:嵌入式开发必须开,90%的隐蔽bug在警告里都有提示 3.5 编译是怎样进行的
编译不是一步到位的,整个过程分成4个严格串行的阶段:
- 预处理阶段:处理所有
#include(把头文件展开到当前C文件里)、#define宏替换、删除注释、根据#if条件编译裁剪代码,生成纯C中间文件.i- 编译阶段:对预处理后的C代码做语法分析、语义检查、各级优化,最终生成汇编语言文件
.s,这是整个流程最核心的一步- 汇编阶段:把汇编代码逐行翻译成二进制机器指令,生成目标文件
.o。这时候文件里已经是纯机器码了,但是所有函数、全局变量的地址还是占位符——因为编译器不知道最后这些东西会被放到Flash/RAM的哪个位置- 链接阶段:把所有
.o文件、HAL库文件、启动文件拼到一起,按照链接脚本的地址规划,给每个函数、变量分配最终的物理地址,把所有占位符替换成真实地址,生成最终的可执行文件.axf常见报错对应:
你编译的时候看到的
undefined reference to xxx就是链接阶段报的——你调用了一个函数,但是链接器在所有.o和库里都找不到这个函数的实现,要么是你没加对应的.c文件,要么是函数名拼错了。3.6 生成了哪些文件
编译完成后,输出目录里会生成一堆中间文件,每个文件都有明确用途:
.o:单文件编译的中间产物,最后会被链接器打包.axf:带完整调试符号的可执行文件,包含所有函数名、变量名、地址映射,调试的时候用这个.hex:Intel HEX格式的文本文件:把地址和数据编码成ASCII字符,纯文本方便传输和校验,烧录器读这个文件写Flash.bin:纯二进制裸镜像:就是Flash里最终要存的原始字节流,没有任何地址和格式信息,体积比hex小30%左右.map:内存映射表:记录每个函数、变量最终被放到了哪个地址、占多少空间,排查Flash溢出、HardFault反推地址的时候全靠它。你看到HardFault反汇编里的地址,去map文件里一搜,马上就知道是哪个函数里出的问题.sct:链接脚本:告诉链接器“代码段要放在Flash起始地址0x08000000,占多少空间;全局变量要放在RAM起始地址0x20000000,栈顶设为多少”
关键区别:
hex是带地址信息的文本格式,可以单独烧某一段地址;
bin是纯二进制流,必须从指定起始地址开始连续烧写。
4. STM32 程序的烧录
4.1 关于“仿真器”
仿真器就是ST-Link、J-Link这类硬件调试器:
一头插电脑USB,一头接STM32的SWD接口,它有两个核心作用:
- 烧录:把电脑上的hex/bin文件,通过SWD协议写到芯片内部Flash里
- 调试:和芯片里的调试接口通信,读寄存器、读内存、下断点、单步执行,让你能实时看到程序跑到哪一步了
4.2 核心问题
STM32内部Flash的物理特性决定了烧录不能像写RAM一样随便写:
- Flash写之前必须先擦除:擦除后整个页变成全1(0xFF),写入只能把1改成0,不能反过来把0写成1
- Flash写入是半字(16位)粒度,不支持单字节写入
- Flash擦写期间,整个Flash接口是忙的,总线上锁死,CPU不能从Flash里取指令——这也是为什么烧录程序的时候,芯片必须先停住,不能边跑边写自己存代码的Flash。
4.3 烧录细节流程
一次完整的烧录,背后做了这几件事:
- ST-Link拉低NRST复位引脚,让CPU立刻停下来,进入停止状态
- ST-Link通过SWD线和芯片内部的调试端口握手,读取芯片ID,确认连接正常
- 通过调试接口控制Flash控制器:解锁Flash,按页擦除需要写入的Flash区域
- 按地址顺序,把hex里的字节流按半字粒度,逐批写到Flash对应地址
- 写完之后读回Flash里的内容,和hex里的原始数据逐字节比对,校验写成功
- 校验通过后释放NRST,芯片从复位状态启动,开始跑刚烧进去的新程序
5. SWD协议
5.1 协议定位与标准
- SWD(Serial Wire Debug,串行线调试) 由 ARM 公司定义,规范承载于 ARM Debug Interface v5(ADIv5,文档号 IHI 0031),是 DAP(Debug Access Port,调试访问端口)对外的两种物理接口之一(另一种是 JTAG-DP)。
- 它本质上是一个主机(调试器)发起、同步、半双工的串行协议:时钟完全由主机通过 SWCLK 提供,目标芯片始终是被动响应方。
- 数据位 LSB First(低位先发);输出方在 SWCLK 下降沿切换数据,输入方在上升沿采样。
- 典型时钟频率 1~4 MHz,上限由具体芯片实现决定,没有统一强制值。
5.2 硬件接口与电气
信号 方向 作用 SWDIO 双向 数据输入/输出,半双工;板上通常需 100 kΩ 左右上拉 SWCLK 主机 → 目标 串行时钟,空闲为低;建议下拉防止毛刺 GND — 共地,必需 VREF 目标 → 主机 目标 IO 电压参考,用于调试器电平匹配 SWO 目标 → 主机(可选) 单线 trace 输出(ITM printf 等) nRESET 主机 → 目标(可选) 硬件复位控制,便于连接复位态芯片 电气要点:
SWDIO 是推挽驱动而非开漏;
TRN(Turnaround)周期内双方都不驱动,靠上拉电阻维持电平,完成总线方向交接。
最小系统接线只需要 SWDIO、SWCLK、GND 三根(调试器另有供电时),这也是 SWD 相对 JTAG 最大的板级优势。
5.3 芯片内部架构:DAP = DP + AP
调试器并不是直接"碰"内核,而是访问芯片内的 DAP,分两层:
- SW-DP(Debug Port):负责 SWD 物理层协议、应答、上电控制、通道选择,只含少量 DP 寄存器。
- AP(Access Port):最常见的是 MEM-AP / AHB-AP,把调试请求转换成 AHB/APB 总线传输。主机借它可以读写内核视角下的任意内存映射地址——SRAM、Flash、外设寄存器、Cortex-M 调试组件(如 DHCSR,0xE000_EDF0)。
- 对内核的启停、单步、断点等操作,就是通过 MEM-AP 读写 DHCSR、DCRSR、DCRDR 等内核调试寄存器实现的;Flash 烧录则是写 Flash 控制器寄存器 + 轮询忙标志完成,没有特殊的"下载协议"。
5.4 请求帧逐位解析
每次传输由主机发出 8 位请求,字段如下(按发送顺序):
位 字段 含义 bit0 START = 1 帧起始标志,固定为 1 bit1 APnDP 0:访问 DP 寄存器;1:访问 AP 寄存器 bit2 RnW 0:写;1:读 bit3/4 A2/A3 寄存器地址的高 2 位(字对齐,A1A0 隐含为 0) bit5 PARITY bit1~bit4 这 4 位的偶校验(1 的个数为奇数则置 1) bit6 STOP = 0 固定 0 bit7 PARK = 1 总线停车位,主机释放由上拉拉高,固定 1 请求后经过 1 个 TRN 周期,目标返回 3 位 ACK(同样 LSB 先发):
ACK 编码(发送序) 含义与处理 OK 001 正常,继续数据相位 WAIT 010 端口忙,主机应重试同一传输 FAULT 100 出错(如总线错误、掉电),需写 DP 的 ABORT 寄存器清除 无应答 111/高阻 SWD 未使能或链路异常,需重新线复位 数据相位为 32 位数据 + 1 位奇偶校验位(32 位数据的偶校验)。
读传输末尾再追加 1 个 TRN,把 SWDIO 驱动权交回主机。
5.5 DP / AP 关键寄存器
DP 寄存器(地址由 A3:A2 选择)
![]()
MEM-AP 寄存器(通过 SELECT 选 Bank 后访问)
![]()
重要机制——AP 读流水线:
发起 AP 读操作时数据不会立刻返回,主机需再发起一次 DP 读 RDBUFF,才能取出上一次 AP 读的结果。这是手写 SWD 主机程序最容易踩的坑。
5.6 建链与初始化完整流程
- 线复位(Line Reset):主机保持 SWDIO 为高,连续输出至少 50 个 SWCLK 周期,使无论处于什么状态的 DP 都回到已知初始态。
- JTAG→SWD 切换(引脚复用 JTAG 的芯片需要):发送 16 位魔法序列 0x79E7(LSB First),再补一次 ≥50 周期线复位;纯 SWD 接口芯片可省略。
- 读 IDCODE:返回 0x1BA01477 之类的 ARM SW-DP 识别码,证明链路通。
- 清错与上电:写 ABORT 清 sticky 错误;写 CTRL/STAT 置 CDBGPWRUPREQ(bit28)和 CSYSPWRUPREQ(bit30),回读确认对应 ACK 位已应答。
- 选 AP 并配置:写 SELECT 选定 MEM-AP 和 Bank,写 CSW 设位宽/递增,写 TAR 设目标地址。
- 数据传输:写 DRW 即向目标地址写数;读则先读 DRW 再读 RDBUFF 取数。之后即可烧 Flash、读写内存、控制内核调试。
5.7 SWO:可选的单线 Trace
SWO 严格说不属于 SWD 协议本体,而是 SWD 口上常见的配套 trace 通道,单向(目标→主机),用于 ITM 打印(替代 UART printf)、DWT 事件、时间戳等,不占用任何用户外设。两种物理编码:
- UART/NRZ 模式:固定波特率,主机用普通 UART 采样即可,最常用;
- Manchester 模式:时钟内嵌在跳变中,无需双方约定波特率。
5.8 SWD 与 JTAG 对比
维度 SWD JTAG 信号线 2 线(SWDIO/SWCLK),+SWO 可选 4~5 线(TCK/TMS/TDI/TDO,可选 nTRST) 双工 半双工(一根数据线) 全双工(TDI/TDO 分开) 拓扑 点对点;ADIv5.2 支持 TARGETSEL 多 drop 天然支持 TAP 菊花链 特长 引脚少、板级布线省,适合 MCU 调试 边界扫描测试、多 TAP、Cortex-A/复杂 SoC 引脚复用 SWCLK=TCK、SWDIO=TMS,多数芯片同组引脚可切换 同左 典型场景 STM32/GD32 等 Cortex-M 下载调试 FPGA、Cortex-A、产线边界扫描 5.9 STM32 实战要点
- ST-Link/J-Link 标准 4 线接 **SWDIO、SWCLK、GND、3.3V(或 VREF)**即可,SWO 需要时再接 PB3(Trace SWO)。
- 低功耗/休眠芯片调试失败时,连接 nRESET 线,调试器用"硬件复位下连接"模式;读不出 IDCODE 优先检查接线、共地、SWCLK 频率是否过高。
- 芯片被配置成关闭调试口、或进了低功耗模式导致失联时,可在 nRESET 释放瞬间用调试器紧急连接,再通过 MEM-AP 改回选项字节/Flash。
- 杜邦线过长导致信号反射时,降低 SWCLK 频率通常即可解决。
5.10 高频面试题速答
SWD 最少几根线?
3 根(SWDIO、SWCLK、GND),标准调试器连接 4 根含电源参考。
SWDIO 一根线怎么双向通信?
半双工 + TRN 周期:传输方向切换前释放总线一个时钟周期,由上拉保持电平,另一方接管驱动。
ACK 有哪几种?收到 FAULT 怎么办?
OK/WAIT/FAULT;FAULT 后写 DP 的 ABORT 寄存器清 sticky 错误再重试。
奇偶校验怎么算?
请求帧对 APnDP、RnW、A2、A3 四位做偶校验;数据相位对 32 位数据做偶校验。
为什么读 AP 寄存器要再读 RDBUFF?
AP 读是流水线操作,结果延迟一个事务返回,缓冲在 RDBUFF 中。
0x79E7 是什么?
JTAG-DP 与 SW-DP 复用引脚时的 JTAG→SWD 切换魔法序列,16 位 LSB 先发。
线复位怎么做?
SWDIO 保持高电平,给至少 50 个 SWCLK 周期。
SWD 是全双工吗?
不是,SWDIO 半双工;JTAG 才是全双工。
SWO 两种编码?
UART(NRZ) 和 Manchester,前者按波特率采样,后者内嵌时钟。
多颗芯片能否挂同一 SWD?
传统 SW-DP 不支持菊花链;ADIv5.2 起可用 TARGETSEL 实现多 drop 寻址。
烧 Flash 的原理?
没有专用下载通道,全部通过 MEM-AP 像普通内存访问一样写 Flash 控制器寄存器并轮询完成。
SWCLK 接哪个 JTAG 引脚?
SWCLK 复用 TCK,SWDIO 复用 TMS。
一句话总结:
SWD 用"一根双向数据线 + 一根主机时钟",以"8 位请求帧 → TRN → 3 位 ACK → 32 位数据 + 校验"的固定事务格式,经 DP/AP 两层把调试请求转成芯片内部总线访问,以最少引脚实现了对 Cortex-M 内核和全部内存空间的完全控制。
STM32 从你点Download到程序跑起来,中间都做了什么?
答:
ST-Link拉NRST复位让CPU停转 → SWD和芯片调试端口握手 → 把Flash烧录算法下载到SRAM → 按页擦除目标Flash区域 → 把hex里的数据逐半字写入Flash → 读回校验数据 → 释放NRST复位芯片 → 硬件自动从0x08000000加载栈顶和复位地址,跳转到Reset_Handler,最后进入main函数。
为什么写内部Flash的时候,烧录算法必须放在RAM里跑?
答:
Flash执行擦写操作的时候,内部Flash接口会被硬件独占,总线会进入stall锁死状态,此时CPU无法从Flash里取指令。
如果烧录代码存在Flash里,写到一半CPU要取下一条指令的时候总线被占死,直接硬件异常,所以必须把烧录代码放到RAM里执行。
SWD相比JTAG有什么优势?
答:
SWD只需要SWDIO、SWCLK两根信号线,加上复位和地一共4根线,占用IO少;
JTAG需要至少10个引脚,PCB布线和引脚占用成本高。SWD完全覆盖了JTAG的烧录、断点、单步调试功能,是Cortex-M系列的标准调试接口。
.hex文件和.bin文件有什么区别?
答:
.hex是带地址信息的ASCII文本格式,每条记录都包含地址和数据,可以单独烧写任意地址段,方便校验和传输;.bin是纯二进制裸镜像,没有任何地址信息和格式头,必须从固定起始地址连续烧写,体积比hex小。
为什么STM32向量表里的中断地址最后一位是1?
答:
Cortex-M内核全程运行Thumb指令,地址最低位的1是Thumb状态标记。
复位后CPU读到这个地址,看到最低位是1,就知道接下来取的是Thumb指令;
如果是0会直接触发硬件错误。
编译的时候-O0和-Os有什么区别?产品发布应该用哪个?
答:
-O0是零优化,所有变量都保留在内存里,方便调试,但是代码体积大、执行慢;
-Os是优化代码体积,在不影响核心性能的前提下把代码压到最小。
嵌入式产品发布默认用-Os,兼顾Flash空间占用和执行效率,调试阶段才用-O0。
烧录的时候把SWD引脚配成GPIO了,连不上芯片怎么办?
答:
在仿真器里选择“连接时复位”,在芯片刚复位、你的用户代码还没来得及把PA13/PA14配成GPIO的窗口期,快速连上调试接口,然后执行整片擦除,之后就能正常连接了。
芯片复位后硬件自动做了哪两件事?
答:
第一,从0x08000000读取第一个32位数写入主栈指针MSP,完成栈初始化;
第二,从0x08000004读取复位中断向量地址写入PC,从这个地址开始执行程序,这两步是硬件自动完成的,不需要软件代码干预。
STM32的三种启动模式有什么区别?什么时候用串口ISP烧录?
答:
BOOT0=0是从主Flash启动,跑用户程序;
BOOT0=1、BOOT1=0是从系统内置Bootloader启动,这时候可以通过串口直接接收hex数据写Flash,不需要仿真器——适合批量生产的时候,板子已经焊好没留仿真器接口的场景;BOOT0=1、BOOT1=1是从SRAM启动,速度快但是掉电丢程序,适合临时调试。
链接时报
undefined reference to xxx是什么原因?答:
这是链接阶段的错误:你代码里调用了xxx函数,但是链接器在所有参与链接的.o文件和库文件里都找不到这个函数的实现。
通常是三种情况:
①对应的.c文件没加到工程里参与编译;
②函数名拼错了,或者声明和实现的名字不一致;
③函数所在的库文件没链接进来。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2602_95649725/article/details/170050459














