单片机上电到 main 之间发生了什么?

单片机上电到 main 之间发生了什么?

你按下开发板的复位键,程序从 main 开始跑——但 main 之前发生了什么?其实 CPU 先要经历一段"没人教过你"的隐秘旅程:从地址 0 取栈指针、跳复位向量、初始化数据段、配置时钟树……走完才轮到你写的 main。今天讲透这段旅程。

一、上电瞬间:CPU 只认两个地址

ARM Cortex-M 内核复位后,硬件做的第一件事非常"死板":从地址 0 取初始栈指针(SP),从地址 4 取复位向量(Reset_Handler 的地址)

地址 0x00000000: 0x20001000   ← 初始 SP(指向 RAM 顶部)
地址 0x00000004: Reset_Handler ← 程序入口地址

注意:入口不是 main,而是 Reset_Handler——这就是很多人困惑的"为什么 main 不是真正的入口"。这些内容由链接脚本和启动汇编文件安排(在 Cortex-M 的启动文件 startup_xxx.s 里)。

二、Reset_Handler:给 C 语言铺路

Reset_Handler 是汇编写的,做三件 C 语言"默认需要但硬件不提供"的事:

  1. 复制 .data 段:已初始化的全局变量(如 int x = 5)存在 Flash,但运行时要在 RAM——启动代码把它们从 Flash 搬到 RAM
  2. 清零 .bss 段:未初始化的全局变量(如 int arr[100])在 C 标准里必须初始化为 0——启动代码把这块 RAM 清零
  3. 调用 SystemInit:配置时钟树(PLL 倍频)、使能外设时钟——这一步决定了芯片跑多快

做完这些,才调用 main()

三、代码演示:模拟启动全流程

class MCU:
    def __init__(self, flash=None):
        # Flash 前两个字: 初始 SP 和复位向量
        self.flash = flash or {0x0000: "SP=0x20001000", 0x0004: "Reset_Handler"}
        self.sp = None; self.pc = None

    def power_on(self):
        print("[上电] 从地址 0 取初始 SP, 从地址 4 取复位向量")
        self.sp = self.flash[0x0000].split("=")[1]
        self.pc = self.flash[0x0004]
        print(f"  SP = {self.sp}, PC = {self.pc}")
        self.reset_handler()

    def reset_handler(self):
        print("[Reset_Handler] 复制 .data 段到 RAM, 清零 .bss 段")
        self.system_init()

    def system_init(self):
        print("[SystemInit] 配置时钟树 (PLL), 使能外设时钟")
        self.main()

    def main(self):
        print("[main] 用户代码开始执行 ✓")

MCU().power_on()

运行输出:

[上电] 从地址 0 取初始 SP, 从地址 4 取复位向量
  SP = 0x20001000, PC = Reset_Handler
[Reset_Handler] 复制 .data 段到 RAM, 清零 .bss 段
[SystemInit] 配置时钟树 (PLL), 使能外设时钟
[main] 用户代码开始执行 ✓

上电 → 取 SP/PC → Reset_Handler(数据段初始化)→ SystemInit(时钟)→ main——这就是每一颗单片机开机时都在走的固定路线。

四、为什么要懂启动流程?

  1. 调试"程序跑不起来":卡在启动阶段(时钟没配对、Flash 等待周期不对)时,main 里的断点根本打不到
  2. 优化启动时间:.bss 清零和 .data 复制在大 RAM 芯片上耗时明显,可优化
  3. 写 Bootloader:需要自己控制向量表偏移(SCB->VTOR)和跳转逻辑
  4. 理解链接脚本section 怎么布局、栈顶在哪,全在这里体现

五、避坑清单

  1. 入口不是 main:复位向量指向 Reset_Handler,main 是被它调用的
  2. 栈顶地址要合法:初始 SP 必须指向 RAM 范围内且对齐,否则一入栈就 HardFault
  3. .bss 不清零是隐患:某些情况下编译器依赖"全局变量为 0",跳过后会出现诡异 bug
  4. 时钟没配就跑外设 = 白等:SystemInit 之前,外设时钟可能是关闭状态
  5. 向量表可偏移:带 Bootloader 时,应用固件要把向量表搬到自己的起始地址并设置 VTOR

六、想系统学 ARM Cortex-M?

本文精选自 ima 知识号【Kruptos】《ARM Cortex-M 技术详解》订阅库(第 011 期复位与启动从地址 0 到 Reset_Handler、第 058 期启动代码与 SystemInit 等 100 期系统教程,从内核架构、寄存器、异常模型到外设驱动与工程实战,每期配可运行代码)。

📚 完整系列 100 期 + 配套代码,已在 ima 知识号发布

本文只是系列的一个切片。完整系列(100 期系统教程 + 每期可运行代码)在 ima 知识号【Kruptos】持续更新中:

  • 🗂 68+ 技术知识库:信号与系统、SDR 软件无线电、数字信号处理、操作系统、AI Agent、大模型微调……几乎覆盖全部软硬件技术栈
  • 🧠 8 款 AI 技能:系列生产、知识库管理、CMMI 受管开发、自进化 Agent 等,已在 ima 技能广场上架,即装即用
  • ✅ 全部免费订阅,后续更新自动推送

🔍 订阅方式:打开 ima(腾讯智能工作台)→ 搜索「Kruptos」→ 一键订阅。或在 ima 内直接搜索《ARM Cortex-M 技术详解》等知识库名称。

💬 你调试过启动阶段的问题吗?卡在哪一步?评论区聊聊——想看 Bootloader 还是链接脚本,点赞高的安排。


作者:Kruptos(西电毕业,13 年无线通信/DSP/嵌入式科研)|原创内容,转载注明出处

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值