00-bootloader前置知识
00-bootloader前置知识
微机原理
堆栈指针SP(Stack Pointer)
“大家想象一下,单片机在运行代码,就像是你在读一本极厚的小说(这本小说就是 Flash 存储器)。
你的眼睛不可能一次性看完一整页,你必须一行一行地读。为了防止读串行,你会用一根食指指着当前正在读的那一行字。读完一行,手指就往下挪一行。
这根用来指路的食指,就是 PC(程序计数器)!
PC 里面存的永远是一个地址,也就是告诉你:‘CPU 老大,下一句该执行哪一行代码了?’
如果突然来了一个电话(中断),你该怎么办?你会把这根手指当前指的位置,写在一张纸条上(保存现场),然后跑去接电话。接完电话回来,看一眼纸条,手指重新指回刚才那行,继续往下读!如果你强行把这根手指掰到一个全是乱码的空白页(比如指到了 0x00000000),你就会瞬间懵逼死机,这就是 HardFault!”
总结:PC寄存器的值就是下一条要执行的代码的位置,非常重要!!
程序计数器PC(Program Counter)
“看书的时候,你偶尔会遇到几道数学题(调用函数)。你要做题,就必须拿出一个草稿本(这就是 SRAM 内存)。
在草稿本上演算时,你会依次写下临时变量(局部变量):a=1, b=2。
为了知道草稿本现在写到哪一行了,免得新写的数据覆盖掉老数据,你会用一支红笔,在最后一行数据的下方画一条横线。
这条划定草稿本边界的红线,就是 SP(堆栈指针)!
每次你进入一个新的函数,定义了新的局部变量,SP 这条红线就会往下挪(压栈,开辟新空间)。每次你算完一道题,退出函数了,那些草稿数据就没用了,SP 这条红线就会瞬间收回上去(弹栈,回收空间)。
为什么咱们在 Bootloader 里把 SP 切飞了就会死机?因为 CPU 正准备去找草稿本上的变量 a 呢,你突然把它的草稿本给抢走,换成了一本全新的空白日记本!它找不到数据,当场就崩溃了!”
总结:SP寄存器的值指向当前的堆栈的位置,只有知道它,CPU才知道要把数据放到哪
问题1
问:内存(sram)那么大,随便找个地方放不行吗,非要放栈里吗?
答:是为了充分利用内存,比如有些函数运行完后不能让他们一直占着内存,需要释放掉,但是怎么知道那些需要被释放呢,所以使用栈这种后进先出的结构,后运行的函数最先释放掉其占用的内存。
问题2
问:栈是什么东西啊,在单片机的哪啊?
答:栈就是单片机上的一个内存(sram)区域,只不过独为cpu钟爱,将其称为栈。
问题3
问:平时用不到啊,根本没有接触过,在哪能看到它呢?
答:栈和堆会在启动文件里给你配置好:
在这可以设置大小
keil调试基础
调试时务必使用-O0优化
圈点的地方务必认真看,每个都会用到
keil菜单讲解
程序下载位置设置

二进制文件获取

flash擦除

编译优化
-O0:零优化(刚入职的实习生)
- 它的行为:绝对听话,极其死板。你 C 语言怎么写,它就一行一行怎么翻译成汇编。你定义了一个变量,它就老老实实去内存(栈)里开辟空间,哪怕这个变量只用了一次。
- 优点:调试的绝对王者。因为代码是 1:1 翻译的,你在打断点、单步调试、查看局部变量时,一切都完美对应,绝不会出现“变量被优化掉”或者“断点乱跳”的情况。
- 缺点:生成的 Bin 文件极其巨大,运行速度最慢。
- 适用场景:日常开发和 Debug 阶段。
-O1:基本优化(熟练的产线工人)
- 它的行为:开始动脑子了。它会把一些明显的废话(永远执行不到的死代码)删掉;更重要的是,它开始大量使用 CPU 的寄存器来保存局部变量,而不是每次都慢吞吞地去读写内存(栈)。
- 优点:代码体积开始缩小,运行速度明显提升。同时,它保留了基本的调试能力。
- 缺点:比上不足,比下有余。
- 适用场景:既想代码跑得快一点,又需要偶尔 Debug 的中间状态。
-O2:标准高级优化(车间主任)—— 工业界的最爱
- 它的行为:编译器彻底放飞自我。它会进行指令重排(打乱你写代码的顺序,以迎合 CPU 的流水线)、常量折叠(把你写的
3 * 24 * 60编译时直接算成4320填进去)。只要它觉得怎么执行快,它就怎么改你的逻辑(但保证最终结果一样)。 - 优点:运行速度极快,同时体积控制得也不错。
- 缺点:Debug 的噩梦。如果你在 O2 下单步调试,你会发现断点在代码行之间上下横跳,局部变量全部显示
<optimized out>(被优化掉了,没放进内存)。 - 适用场景:产品 Release(发布)时的默认标准。
-O3:性能狂魔(疯狂的科学家)
- 它的行为:为了极致的运行速度,可以牺牲一切(包括代码体积)。它会干两件疯狂的事:
- 函数内联(Inlining):把你写的小函数,直接像宏定义一样复制粘贴到调用的地方,省去了函数调用的跳转开销,但代码体积会膨胀。
- 循环展开(Loop Unrolling):把你写的
for(i=0; i<4; i++) { do_something(); },直接强行展开成 4 句do_something();,省去了i++和条件判断的时间。
- 优点:单片机能跑出的最高执行速度。
- 缺点:Bin 文件体积显著膨胀(空间换时间)。
- 适用场景:需要跑极其复杂的算法(如电机 FOC 控制、DSP 音频处理、FFT 运算)的专属核心代码段。
-Ofast:法外狂徒(不要命的赛车手)
- 它的行为:在
-O3的基础上,打破 C 语言和 IEEE 国际标准。特别是针对浮点数(float/double)运算,它会为了速度忽略所有安全检查(比如不管非法的NaN和无限大Inf,强行改变数学运算的结合律)。 - 优点:浮点数数学运算快到起飞。
- 缺点:极度危险。如果你的数学公式里有可能出现除以 0 或者溢出,程序会跑出诡异的结果。
- 适用场景:纯粹的数字信号处理,且你 100% 确保输入数据是合法的。
🌟 终极省空间绝招:大小之争
对于我们写 Bootloader 来说,速度根本不重要(跳转就那几微秒),体积才是王道。这时候就要用到针对体积的优化:
-Os:空间魔术师(精打细算的管家)
- 它的行为:启用所有
-O2级别的优化,但坚决拒绝任何会增加代码体积的优化行为(比如它绝对不会像 O3 那样去展开循环或者内联大函数)。 - 优点:在保持较高运行速度的同时,尽可能缩小 Bin 文件体积。
- 适用场景:Flash 空间比较紧张的常规 App 工程。
-Oz / -Omax:黑洞级压缩(榨干最后一滴血)
(注:-Oz 是 Keil AC6 / Clang 编译器专属的极致空间优化;-Omax 通常是 Keil GUI 里的叫法,代表开启了 LTO 跨模块优化的极限状态)
- 它的行为:为了缩小体积,可以牺牲运行速度。它会把所有长得差不多的代码段,强行提取成一个公共函数供大家调用(哪怕跳转会浪费一点时间)。它会用一切极其晦涩、短小的汇编指令来替代常规指令。
- 优点:单片机界最小的 Bin 文件,没有之一。
- 缺点:代码执行速度会变慢,且绝对无法 Debug。
- 适用场景:你的 Bootloader! 因为 Bootloader 的专属 Flash 扇区往往只有 8KB 到 16KB,必须用
-Oz把它压到极致
总结
| 优化等级 | 核心目标 | 体积 (Size) | 速度 (Speed) | 调试难度 | 一句话总结 |
|---|---|---|---|---|---|
| -O0 | 方便调试 | 极大 | 最慢 | ⭐ (极易) | 开发阶段的保姆 |
| -O1 | 均衡过渡 | 中等 | 中等 | ⭐⭐ | 稍微聪明的保姆 |
| -O2 | 全局均衡 | 较小 | 极快 | ⭐⭐⭐⭐ | 工业发布的标准 |
| -O3 | 空间换时间 | 膨胀 | 神速 | ⭐⭐⭐⭐⭐ | 算法狂魔的最爱 |
| -Os | 兼顾体积 | 很小 | 较快 | ⭐⭐⭐⭐ | Flash 告急时的救星 |
| -Oz | 时间换空间 | 极小 | 较慢 | ⭐⭐⭐⭐⭐ (地狱) | Bootloader 专属榨汁机 |
- o0:零优化,生成的 Bin 文件极其巨大,运行速度最慢,用于日常开发和 Debug 阶段。
- o1:基础优化,既想代码跑得快一点,又需要偶尔 Debug 的中间状态,不建议使用。
- o2:打乱你写代码的顺序,以迎合 CPU 的流水线、把你写的
3 * 24 * 60编译时直接算成4320填进去。只要它觉得怎么执行快,它就怎么改你的逻辑,运行速度极快,同时体积控制得也不错,但是无法Debug。 - o3:为了极致的运行速度,可以牺牲一切(包括代码体积),不能debug,不要用于Bootloader。
- ofast:在
-O3的基础上,打破 C 语言和 IEEE 国际标准,运行时可能会出现错误,但是浮点数数学运算快到起飞。 - oz/omax:为了缩小体积,可以牺牲运行速度,无法Debug,但是体积小,适用于Bootloader。
启动流程


STM32 内部的大脑是 ARM Cortex-M 内核(比如 F103 是 M3)。ARM 官方给这个内核定下了一条极其死板的“出厂铁律”: 芯片每次上电复位,CPU 的眼睛只会死死盯住 0x00000000 这个地址!
它会固定做两件事:
2. 去 0x00000000 读出一个 32 位的数据,赋值给 SP(主堆栈指针)。
3. 去 0x00000004 读出一个 32 位的数据,赋值给 PC(程序计数器,也就是 Reset_Handler 复位函数的入口)。
ARM 规定必须去 0x00000000 找启动代码,但是 ST(意法半导体)在造 STM32 的时候,把用来存代码的物理 Flash 芯片,焊在了总线的 0x08000000 这个位置。这就尴尬了:CPU 非要去 0x00000000 找代码,但代码实际存在 0x08000000。怎么骗过 CPU 呢?
ST 发明了一个叫 内存重映射(Aliasing) 的机制。你可以把它想象成一面“镜子”。 当你的画板配置了从 Flash 启动时,硬件会在 0x00000000 放置一面镜子,直接反射出 0x08000000 的内容。
- CPU 以为自己在读
0x00000000,其实它读到的是0x08000000里的栈指针。 - CPU 以为自己在读
0x00000004,其实它读到的是0x08000004里的函数地址。
这就是为什么你把代码烧录到0x08000000,单片机却能完美启动的原因。
为什么要弄这么复杂的“镜子”?
ST 之所以大费周章地搞这个“镜子”,是为了让你能通过硬件引脚(BOOT0 和 BOOT1)来自由决定单片机从哪里启动!
这面位于0x00000000的“镜子”,是可以随时转向的: - 当 BOOT0 = 0 时(正常模式): 镜子照向
0x08000000(主 Flash)。单片机上电运行你写的代码。 - 当 BOOT0 = 1, BOOT1 = 0 时(串口下载模式): 镜子照向
0x1FFFF000(系统存储器 System Memory)。这里面固化了 ST 出厂时写死的一段 Bootloader,专门用来通过串口(ISP)接收你电脑发来的代码,并烧到 Flash 里。(这就是为什么你板子空着也能用串口烧代码的原因),也可以救被锁死的板子。 - 当 BOOT0 = 1, BOOT1 = 1 时(调试模式): 镜子照向
0x20000000(内部 SRAM)。代码直接从内存里启动,常用于解除 Flash 读写锁或者极限速度的仿真调试。