Brilliantwxx头像
【STM32】 程序从源码到运行:编译、烧录与启动全流程详解(上)封面图

【STM32】 程序从源码到运行:编译、烧录与启动全流程详解(上)


  STM32 程序从源码到运行:编译、烧录与启动全流程详解   

1. 整体认识

我们写STM32程序的完整链路本质上是一条翻译+搬运+启动的流水线:

        你在编辑器里敲下的人类可读C代码 → 经过工具链一步步翻译成CPU能识别的二进制机器码 → 通过调试器硬件把二进制写到芯片内部的非易失Flash里 → 芯片上电复位后,硬件自动从Flash里取指令,把程序跑起来。

整个流程可以拆成三个核心阶段:

  1. 编译阶段:把C源码转成可烧录的镜像文件,解决“CPU看不懂人话”的问题
  2. 烧录阶段:把生成的镜像通过硬件接口写到芯片内部Flash,解决“程序怎么进芯片”的问题
  3. 启动阶段:芯片上电后自动加载程序到运行状态,解决“芯片怎么从头跑起来”的问题

        平时我们点一下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执行一条指令的标准流程是经典的三级流水线:

  1. 取指:程序计数器PC指向下一条指令的Flash地址,CPU通过内部总线把指令从Flash读到指令缓存
  2. 译码:指令译码电路把读到的二进制位翻译成对应的控制信号,告诉ALU、寄存器堆、总线控制器要做什么操作
  3. 执行:硬件按控制信号完成运算,更新寄存器或内存的值,同时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个严格串行的阶段:

  1. 预处理阶段:处理所有#include(把头文件展开到当前C文件里)、#define宏替换、删除注释、根据#if条件编译裁剪代码,生成纯C中间文件.i
  2. 编译阶段:对预处理后的C代码做语法分析、语义检查、各级优化,最终生成汇编语言文件.s,这是整个流程最核心的一步
  3. 汇编阶段:把汇编代码逐行翻译成二进制机器指令,生成目标文件.o。这时候文件里已经是纯机器码了,但是所有函数、全局变量的地址还是占位符——因为编译器不知道最后这些东西会被放到Flash/RAM的哪个位置
  4. 链接阶段:把所有.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接口,它有两个核心作用:

  1. 烧录:把电脑上的hex/bin文件,通过SWD协议写到芯片内部Flash里
  2. 调试:和芯片里的调试接口通信,读寄存器、读内存、下断点、单步执行,让你能实时看到程序跑到哪一步了

4.2 核心问题

STM32内部Flash的物理特性决定了烧录不能像写RAM一样随便写:

  1. Flash写之前必须先擦除:擦除后整个页变成全1(0xFF),写入只能把1改成0,不能反过来把0写成1
  2. Flash写入是半字(16位)粒度,不支持单字节写入
  3. Flash擦写期间,整个Flash接口是忙的,总线上锁死,CPU不能从Flash里取指令——这也是为什么烧录程序的时候,芯片必须先停住,不能边跑边写自己存代码的Flash。

4.3 烧录细节流程

一次完整的烧录,背后做了这几件事:

  1. ST-Link拉低NRST复位引脚,让CPU立刻停下来,进入停止状态
  2. ST-Link通过SWD线和芯片内部的调试端口握手,读取芯片ID,确认连接正常
  3. 通过调试接口控制Flash控制器:解锁Flash,按页擦除需要写入的Flash区域
  4. 按地址顺序,把hex里的字节流按半字粒度,逐批写到Flash对应地址
  5. 写完之后读回Flash里的内容,和hex里的原始数据逐字节比对,校验写成功
  6. 校验通过后释放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,分两层:

  1. SW-DP(Debug Port):负责 SWD 物理层协议、应答、上电控制、通道选择,只含少量 DP 寄存器。
  2. AP(Access Port):最常见的是 MEM-AP / AHB-AP,把调试请求转换成 AHB/APB 总线传输。主机借它可以读写内核视角下的任意内存映射地址——SRAM、Flash、外设寄存器、Cortex-M 调试组件(如 DHCSR,0xE000_EDF0)。
  3. 对内核的启停、单步、断点等操作,就是通过 MEM-AP 读写 DHCSR、DCRSR、DCRDR 等内核调试寄存器实现的;Flash 烧录则是写 Flash 控制器寄存器 + 轮询忙标志完成,没有特殊的"下载协议"。

5.4 请求帧逐位解析

每次传输由主机发出 8 位请求,字段如下(按发送顺序):

位字段含义
bit0START = 1帧起始标志,固定为 1
bit1APnDP0:访问 DP 寄存器;1:访问 AP 寄存器
bit2RnW0:写;1:读
bit3/4A2/A3寄存器地址的高 2 位(字对齐,A1A0 隐含为 0)
bit5PARITYbit1~bit4 这 4 位的偶校验(1 的个数为奇数则置 1)
bit6STOP = 0固定 0
bit7PARK = 1总线停车位,主机释放由上拉拉高,固定 1

请求后经过 1 个 TRN 周期,目标返回 3 位 ACK(同样 LSB 先发):

ACK编码(发送序)含义与处理
OK001正常,继续数据相位
WAIT010端口忙,主机应重试同一传输
FAULT100出错(如总线错误、掉电),需写 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 建链与初始化完整流程

  1. 线复位(Line Reset):主机保持 SWDIO 为高,连续输出至少 50 个 SWCLK 周期,使无论处于什么状态的 DP 都回到已知初始态。
  2. JTAG→SWD 切换(引脚复用 JTAG 的芯片需要):发送 16 位魔法序列 0x79E7(LSB First),再补一次 ≥50 周期线复位;纯 SWD 接口芯片可省略。
  3. 读 IDCODE:返回 0x1BA01477 之类的 ARM SW-DP 识别码,证明链路通。
  4. 清错与上电:写 ABORT 清 sticky 错误;写 CTRL/STAT 置 CDBGPWRUPREQ(bit28)和 CSYSPWRUPREQ(bit30),回读确认对应 ACK 位已应答。
  5. 选 AP 并配置:写 SELECT 选定 MEM-AP 和 Bank,写 CSW 设位宽/递增,写 TAR 设目标地址。
  6. 数据传输:写 DRW 即向目标地址写数;读则先读 DRW 再读 RDBUFF 取数。之后即可烧 Flash、读写内存、控制内核调试。

5.7 SWO:可选的单线 Trace

        SWO 严格说不属于 SWD 协议本体,而是 SWD 口上常见的配套 trace 通道,单向(目标→主机),用于 ITM 打印(替代 UART printf)、DWT 事件、时间戳等,不占用任何用户外设。两种物理编码:

  • UART/NRZ 模式:固定波特率,主机用普通 UART 采样即可,最常用;
  • Manchester 模式:时钟内嵌在跳变中,无需双方约定波特率。

5.8 SWD 与 JTAG 对比

维度SWDJTAG
信号线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

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--