很多人在学习 STM32 时都会遇到一个困惑:代码明明是从main()函数开始写的,为什么点下复位按键后,程序却能自动跳转到正确的位置?为什么有时候中断进不去、程序跑飞,查来查去问题出在启动阶段?这些问题的背后,都指向同一个基础知识点——STM32 的启动过程。
这篇文章我会把启动过程完整拆开来讲:从硬件上电开始,一直到main()函数执行,中间每一步做了什么、为什么要做、代码在哪,都会逐个解释。内容主要面向正在准备单片机面试的读者,以及刚接触 STM32、想深入理解底层机制的开发者。
通过这篇文章,你会掌握以下内容:
- STM32 的三种启动方式及 BOOT 引脚配置。
- 中断向量表在启动过程中的作用。
- 启动文件
startup_stm32f10x_hd.s的关键代码逻辑。 - 从复位到
main()的完整调用链。 - 常见启动异常与排查方法。
- 面试中常见的启动过程题目问答思路。
文章中会以 STM32F103 系列为例,因为这是目前学习、面试、项目中接触最多的型号。其他 Cortex-M3/M4 内核的 STM32 启动逻辑基本一致,理解了 F1 也就理解了整个家族的基本框架。
1. 什么是 STM32 启动过程
很多初学者会把“启动过程”想得很神秘,其实它的本质非常朴素:处理器在上电或复位后,如何找到第一条要执行的指令,以及如何把运行环境准备好,最终进入用户写的 C 代码。
无论 PC 还是单片机,都存在这个过程。PC 上有 BIOS/UEFI 引导操作系统,STM32 则通过内核自带的固定机制,从地址0x00000000处读取初始值,完成最基本的初始化。
专业一点说,STM32 基于 ARM Cortex-M 内核,Cortex-M 系列内核在硬件层面规定:上电复位后,处理器会从向量表中取出栈顶地址和复位向量,随后跳转到复位向量指向的代码去执行。这个动作不需要软件参与,是芯片硬件自动完成的。
STM32 的启动过程主要包括三个阶段:
- 硬件初始化:设置栈指针、取复位向量、跳转执行。
- 软件初始化:执行启动文件中的
Reset_Handler,完成向量表拷贝、变量初始化、时钟初始化。 - 进入 C 世界:调用
__main,最终进入main()。
用一张简化的流程表示:
上电/复位 │ ▼ 从 0x00000000 读取栈顶地址 → 赋给 MSP │ ▼ 从 0x00000004 读取复位向量 → 得到 Reset_Handler 地址 │ ▼ 跳转执行 Reset_Handler │ ▼ SystemInit():初始化时钟 │ ▼ 进入 __main:完成 RW 数据拷贝、ZI 区清零、堆栈初始化 │ ▼ 进入 main()整个链路中,启动文件(汇编文件)是核心,后面会专门拆解。
2. 动手前的必备概念:存储结构与启动方式
在分析启动代码之前,有两个概念必须先弄清楚:存储器映射和启动方式。这两个概念在面试中也很常考,而且直接影响你对 BOOT0、BOOT1 引脚的理解。
2.1 STM32 的存储器映射
STM32F103 的内存空间并不是一块内存用到底,而是把 Flash、SRAM、外设寄存器等统一映射到 4GB 的地址空间中。Cortex-M3 内核规定其中一部分地址范围用于特定用途。
常见且重要的地址区域如下:
| 地址范围 | 作用 | 典型用途 |
|---|---|---|
0x08000000 ~ 0x0807FFFF | 片内 Flash | 存放代码、只读常量、初始化的全局变量副本 |
0x20000000 ~ 0x2000FFFF | 片内 SRAM | 存放栈、堆、全局变量、局部变量 |
0x40000000 ~ 0x5FFFFFFF | 外设寄存器 | GPIO、USART、SPI、定时器寄存器 |
0xE0000000 ~ 0xE00FFFFF | Cortex-M3 内部外设 | NVIC、SysTick、调试组件 |
其中最关键的是 Flash 和 SRAM:
- Flash:程序代码存放在这里,上电后 CPU 直接从 Flash 取指令执行。
- SRAM:运行时的数据空间,栈指针 SP 指向这里的某块地址。
很多初学者会问:Flash 和 SRAM 都叫“存储器”,到底有什么区别?简单理解,Flash 就像硬盘,断电不丢数据,但写入慢;SRAM 就像内存,速度很快,但断电数据消失。程序放在 Flash,变量运行时放在 SRAM。
这一知识点直接关系到启动过程:启动文件在做变量初始化时,会把 Flash 中的初始值拷贝到 SRAM 中。
2.2 三种启动方式
STM32F103 通过 BOOT0 和 BOOT1 引脚决定从哪块存储区启动。这个在数据手册中有明确描述,下面用表格整理:
| BOOT0 | BOOT1 | 启动模式 | 说明 |
|---|---|---|---|
| 0 | X | 从主 Flash 启动 | 正常运行模式,绝大多数应用使用 |
| 1 | 0 | 从系统存储器启动 | 用于串口下载程序(ISP) |
| 1 | 1 | 从内置 SRAM 启动 | 调试模式,用于 RAM 中调试代码 |
值得注意的是,Cortex-M3 内核固定从0x00000000取向量表,但 STM32 通过引脚选择把不同存储区域“映射”到了0x00000000这个地址。
也就是说:
- 从 Flash 启动时,地址
0x00000000被映射到 Flash 的首地址0x08000000。 - 从系统存储器启动时,
0x00000000被映射到系统存储器的首地址。 - 从 SRAM 启动时,
0x00000000被映射到 SRAM 的首地址0x20000000。
代码层面的地址不需要变化,因为硬件帮你做了地址重映射。这就是为什么你在 Keil 中看到的代码都是从0x08000000开始的,但 CPU 上电时仍然能正确启动。
2.3 为什么要弄启动方式
启动方式不是一个“多余的设计”。它的实际价值在于:
- 开发阶段:可以通过 BOOT 引脚选择从系统存储器启动,用串口下载程序,不需要额外的下载器。
- 产品生产:批量烧录时可能利用 ISP 方式。
- 调试阶段:可以从 SRAM 启动,加快调试循环,避免频繁擦写 Flash。
在实际项目中,调试完成后 BOOT0 和 BOOT1 通常都接地(选择主 Flash 启动),保证上电直接运行用户程序。
3. 启动文件到底做了什么
项目工程中,启动文件通常叫startup_stm32f10x_hd.s。不同型号芯片对应不同启动文件,比如:
startup_stm32f10x_md.s:中等容量,64KB 到 128KB Flash。startup_stm32f10x_hd.s:高容量,256KB 到 512KB Flash。startup_stm32f10x_xl.s:超高容量,768KB 到 1MB Flash。
如果是 F4 系列,则对应startup_stm32f40xx.s或startup_stm32f429xx.s。
启动文件虽然是汇编写的,但逻辑并不复杂,主要做以下几件事。
3.1 定义栈区和堆区
栈(Stack)和堆(Heap)的大小,在启动文件开头定义。比如:
Stack_Size EQU 0x00000400 AREA STACK, NOINIT, READWRITE, ALIGN=3 Stack_Mem SPACE Stack_Size __initial_sp这段汇编的意思是:
Stack_Size EQU 0x00000400:定义栈大小为 1KB。AREA STACK, NOINIT, READWRITE:开辟一个名为 STACK 的段,不初始化,可读可写。SPACE Stack_Size:保留 1KB 空间。__initial_sp:这个标号指向栈顶,后面会放入向量表的第一项。
堆的定义方式类似:
Heap_Size EQU 0x00000200 AREA HEAP, NOINIT, READWRITE, ALIGN=3 Heap_Mem SPACE Heap_Size __initial_heap_top堆主要给malloc这类动态内存分配函数使用。在裸机开发中如果没用到,大小可以设置得比较小。
这里需要注意:栈的大小直接关系到程序的局部变量、函数调用嵌套深度、中断嵌套深度。如果栈设置太小,程序运行复杂后很容易栈溢出,导致 HardFault。实际项目中要根据需求调整,不要盲目用默认值,也不要随意改大,因为 SRAM 总容量有限。
3.2 定义中断向量表
向量表是启动文件中非常核心的部分,Cortex-M3 内核依赖它完成中断分发。它的本质是一个函数指针数组,里面按顺序存放各个中断服务函数的地址。
起始部分代码(经过精简):
AREA RESET, DATA, READONLY EXPORT __Vectors EXPORT __Vectors_End EXPORT __Vectors_Size __Vectors DCD __initial_sp ; 栈顶地址 DCD Reset_Handler ; 复位向量 DCD NMI_Handler ; NMI 中断 DCD HardFault_Handler ; 硬件错误中断 DCD MemManage_Handler ; 内存管理错误 DCD BusFault_Handler ; 总线错误 DCD UsageFault_Handler ; 使用错误 ; ... 后续是各种外设中断 __Vectors_End如果按地址来看:
| 偏移 | 内容 |
|---|---|
| 0x00 | 初始栈指针__initial_sp |
| 0x04 | 复位向量Reset_Handler |
| 0x08 | NMI 异常入口 |
| 0x0C | HardFault 异常入口 |
| ... | 其他异常和外设中断 |
这是整个启动流程的第一关键:Cortex-M3 复位后,硬件固定从地址 0x00000000 读取栈顶地址,并从地址 0x00000004 读取复位向量地址,然后跳转过去。
3.3 编写复位处理函数
复位处理函数是启动过程中软件真正开始接管硬件的入口。核心代码如下(精简):
Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT SystemInit IMPORT __main LDR R0, =SystemInit BLX R0 LDR R0, =__main BX R0 ENDP这五行的逻辑非常清晰:
- 加载
SystemInit函数的地址到 R0 寄存器。 - 调用
BLX R0执行SystemInit。 - 加载
__main的地址到 R0 寄存器。 - 调用
BX R0跳转到__main。
其中,SystemInit在system_stm32f10x.c文件中实现,主要完成:
- 恢复 RCC 寄存器到复位默认值。
- 配置 Flash 预取缓冲区。
- 设置系统时钟(比如 PLL 倍频到 72MHz)。
- 更新
SystemCoreClock全局变量。
__main不是用户写的main(),而是编译器中 C 库的初始化入口。它做了这些事:
- 把 Flash 中的 RW 数据拷贝到 SRAM。
- 把 ZI 数据段清零。
- 调用
__rt_entry完成 C 运行环境初始化。 - 最后调用用户的
main()。
有人会疑惑:main()之前居然有这么多步骤?为什么不能直接跳进main()?因为你的 C 代码里可能有初始值不为 0 的全局变量,它们在程序编译后存放在 Flash 中,但运行时要放在 SRAM 中;如果不拷贝,全局变量的初始值就是错的。同理,初始值为 0 的全局变量(ZI 区),如果不先清零,运行结果也是未定义的。这些脏活都在__main中完成。
3.4 中断服务函数的弱定义
启动文件的末尾会为每个中断定义一个弱函数:
NMI_Handler PROC EXPORT NMI_Handler [WEAK] B . ENDP这里的[WEAK]关键字意思是“弱定义”。如果你在自己的 C 代码里重新写了同名函数,链接时以你的函数为准;如果没有写,就使用启动文件里这个空函数。
空函数只有一句B .,也就是死循环。这样设计的意义是:任何意外触发的中断,如果用户没有正确处理,程序会停在原地,方便排查;而不是跳到一个非法地址导致硬件错误。
4. 从工程角度看启动链接过程
启动文件不是单独存在的,它要配合链接脚本(分散加载文件)才能正确运行。STM32 工程中常见的.sct文件(Keil 中自动生成)或 GCC 环境下的.ld文件起到这个作用。
STM32F103C8T6_FLASH.sct文件典型内容:
LR_IROM1 0x08000000 0x00010000 { ER_IROM1 0x08000000 0x00010000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00005000 { .ANY (+RW +ZI) } }逐行解读:
LR_IROM1:加载域,描述程序在 Flash 中的分布,起始地址0x08000000,大小 64KB(0x00010000)。ER_IROM1:执行域,代码运行时也在 Flash 中执行(冯诺依曼与哈佛的混合架构下,STM32 支持从 Flash 直接取指执行)。*.o (RESET, +First):链接时把启动文件中 RESET 段放在最开始,也就是向量表要放在 Flash 首地址位置。RW_IRAM1:运行域,RW 数据(已初始化全局变量)和 ZI 数据(未初始化全局变量)放在0x20000000起始的 SRAM 中。
这个链接顺序很重要。如果向量表不放在 Flash 开头,硬件复位后从0x00000000读到的就不是正确的栈顶和复位向量,程序必然跑飞。
在 Keil 工程中,如果你使用的是自定义链接脚本,务必保留+First这段设置,否则会出现下载后程序不运行的诡异现象。
5. 完整实战:借助调试器验证启动过程
下面通过实际调试来验证启动过程每一步是否真的像前面分析的那样。示例平台以常见的 STM32F103C8T6 最小系统板为例,开发环境使用 Keil MDK,调试器使用 ST-Link。
5.1 创建测试工程
新建一个标准库工程,目录结构如下:
stm32_startup_demo/ ├── Core/ │ ├── inc/ │ │ ├── main.h │ │ └── stm32f10x_conf.h │ └── src/ │ ├── main.c │ └── stm32f10x_it.c ├── Driver/ │ └── inc/ ├── Library/ │ ├── inc/ │ └── src/ ├── Startup/ │ └── startup_stm32f10x_hd.s └── Project/实际使用 CubeMX 生成也可以,原理相同。核心是确认启动文件、系统文件、链接脚本都存在。
5.2 编写测试代码
写一个最简单的测试程序,在main()中设置一个 LED 翻转循环:
// 文件路径:Core/src/main.c #include "stm32f10x.h" void Delay(void) { volatile uint32_t i; for (i = 0; i < 1000000; i++); } int main(void) { GPIO_InitTypeDef GPIO_InitStructure; /* 使能 GPIOC 时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); /* 配置 PC13 为推挽输出 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); while (1) { GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET); Delay(); GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET); Delay(); } }这代码本身并不复杂,作用是在 PC13 引脚输出翻转方波(对应最小系统板上的 LED 闪烁)。
5.3 在启动文件设置断点
在 Keil 中,首先编译工程并进入调试模式。然后在启动文件Reset_Handler处设置断点。具体操作:
- 打开
startup_stm32f10x_hd.s文件。 - 找到
Reset_Handler这行。 - 点击行号列的灰色区域设置断点。
- 点击 “Debug” 按钮进入调试模式。
按下复位按钮后,程序会停在Reset_Handler处。这时观察 Keil 右下角的寄存器窗口:
- SP:栈指针已经指向
__initial_sp对应的地址。 - PC:程序计数器停在
Reset_Handler所在地址。
这直接验证了“硬件自动读取向量表并跳转复位向量”这个过程。
5.4 单步跟踪启动代码
点击单步执行,仔细观察:
- 第一步执行
LDR R0, =SystemInit,此时 R0 被加载为 SystemInit 的地址。 - 第二步执行
BLX R0,跳进 SystemInit 内部。这里可以查看 RCC 相关寄存器变化。 - 从 SystemInit 返回后,执行
LDR R0, =__main。 - 执行
BX R0,进入__main。
单步跟踪到__main内部时,看到的都是编译器生成的汇编代码。如果对库函数比较熟悉,可以看到最终会跳到main()。
5.5 观察内存中的初始化效果
为了更直观地理解 RW 数据拷贝,可以在 main.c 中定义一个初始值不为 0 的全局变量:
// 文件路径:Core/src/main.c uint32_t g_test_value = 0x12345678;编译后,你会注意到:
- 在 Flash 地址区域,这个变量对应位置存了
0x12345678。 - 在进入
main()之前,__main把它拷贝到了 SRAM 中。 - 在
main()内部查看变量地址,地址位于0x20000000之后的 SRAM 区。
如果在main()入口处设置断点,然后查看变量的实际地址和值,就能确认启动过程完成了数据拷贝。
5.6 验证时钟初始化
在SystemInit内部设置断点,查看 RCC_CR 寄存器:
- 复位后 RCC_CR 的值通常是
0x00000083,表示 HSI(内部高速时钟)开启。 - SystemInit 执行完毕后,外部 HSE 可能已经启动,PLL 锁相环开始工作。
- 系统时钟最终被配置到 72MHz。
这一步骤验证了“启动代码负责把默认 8MHz 内部时钟切换到外部 8MHz 晶振并倍频到 72MHz”的过程。
6. 常见启动问题排查
启动过程涉及硬件、链接脚本、启动文件、系统初始化多个环节,任何一个环节出问题,程序都跑不起来。下面是实际开发中最高频的问题与排查方法。
6.1 程序下载成功但完全没有运行
| 可能原因 | 排查方向 |
|---|---|
| BOOT0/BOOT1 配置错误 | 确认 BOOT0 = 0,从主 Flash 启动 |
| 链接脚本中向量表没有放在最前 | 检查+First设置 |
| 代码量超出发行容量 | 查看编译输出 ROM 使用量 |
| 复位引脚被外部拉低 | 检查 NRST 引脚电路 |
6.2 一运行就进入 HardFault
| 可能原因 | 排查方向 |
|---|---|
| 栈指针错误 | 检查启动文件 Stack_Size 是否够用 |
| 函数指针非法 | 检查函数指针初始化 |
| 访问了不存在的地址 | 检查外设地址是否正确 |
| 时钟配置失败 | 检查 SystemInit 中 HSE 外部晶振是否正常 |
| 中断服务函数未实现 | 启动文件的弱函数是死循环,检查中断是否意外触发 |
进入 HardFault 后,可以在调试器中查看 Fault 状态寄存器,判断是总线错误、内存管理错误还是用法错误,再针对性地排查。
6.3 有中断但进不去
| 可能原因 | 排查方向 |
|---|---|
| NVIC 未配置使能 | 检查 NVIC_Init 配置 |
| 中断函数名称不匹配 | 启动文件中的弱函数名称必须与中断向量表名称一致 |
| 中断服务函数被优化掉 | 中断函数不要定义为 static |
| 优先级分组配置错误 | 检查 NVIC_PriorityGroupConfig |
6.4 变量初始化值不对
主要原因是启动过程的 RW 数据拷贝或 ZI 区清零没有正确执行。如果使用的链接脚本有问题,或者跳过了__main直接调用main(),就会出现这种问题。
6.5 调试器连不上芯片
可能原因:
- 芯片进入低功耗模式。
- 调试接口被复用为普通 IO。
- BOOT 引脚设置导致代码跳转异常。
解决方案通常是先拉低 BOOT0 让芯片从系统存储器启动,或者按住复位键后点击下载,再松开复位键。某些情况下需要先擦除整个 Flash。
7. 面试回答思路与常见问题
启动过程真的是面试高频考点,下面把常见的面试问题整理一下,并给出回答要点。
7.1 请简述 STM32 的启动过程
回答模板:
STM32 上电后,硬件根据 BOOT 引脚选择启动介质。以主 Flash 启动为例,Cortex-M3 内核从
0x00000000地址读取栈顶地址赋给 MSP,然后从0x00000004地址读取复位向量,得到Reset_Handler的入口地址并跳转执行。Reset_Handler首先调用SystemInit()完成时钟初始化,然后进入编译器的__main,由__main完成 RW 数据拷贝、ZI 区清零、堆栈初始化后,最终调用用户写的main()。
这个回答把“硬件动作”“软件动作”“最终目标”都涵盖了,面试官听到这里通常就会认可底层基础。
7.2 为什么要启动文件?不用启动文件行不行?
回答要点:
启动文件负责构建 C 运行环境。C 代码运行的前提是栈已初始化、全局变量已就位。没有启动文件,程序即便能从 Flash 开始执行,也会因为没有栈指针、没有初始化变量而无法正常工作。理论上可以自己用汇编编写这两段初始化逻辑,但启动文件已经把这部分标准化了,直接使用更可靠。
7.3SystemInit()和main()是什么关系?
回答要点:
SystemInit()是在main()之前被启动文件调用的,主要完成系统时钟初始化。它不是一个用户必须调用的函数,但绝大多数工程都会使用它,因为进入main()之前,芯片运行速度要处于一个确定状态,外设总线频率也要正确。部分低功耗应用会在main()里重新配置时钟,但 SystemInit 中的默认配置可以保证刚上电时程序能够稳定运行。
7.4 中断向量表为什么要从 Flash 开头放?
回答要点:
Cortex-M3 内核规定上电后从
0x00000000读取栈顶地址和复位向量,而 STM32 默认把 Flash 地址0x08000000重映射到0x00000000。因此 Flash 开头必须是向量表,否则处理器第一次取指就取到了错误数据。
7.5 Stack 和 Heap 有什么区别?
回答要点:
Stack 由编译器自动管理,用于函数调用、局部变量、中断现场保存,栈空间向低地址增长;Heap 由程序员通过 malloc/free 管理,向高地址增长,裸机开发中如果不用动态内存,Heap 可以设置得比较小。二者共用一段 SRAM 区域,堆栈溢出会造成程序行为不可预期。
7.6 如果向量表需要放到 RAM,应该怎么做?
回答要点:
Cortex-M3 提供了向量表偏移寄存器 VTOR,通过设置
SCB->VTOR把向量表重定位到 SRAM 指定地址,并将新的向量表拷贝到该地址。这种方式常用于 IAP 在线升级或引导程序场景,需要在启动后手动重映射,同时注意向量对齐要求(Cortex-M3 要求偏移量按 128 字节对齐,M4 要求按 256 字节对齐)。
8. 工程中的最佳实践建议
了解了启动过程之后,实际开发中有几点建议可以提前收藏。
不要随意修改启动文件。标准库和 HAL 库提供的启动文件经过大量验证,修改栈大小、堆大小必须经过评估。如果确实要修改,建议在副本上修改,并保留原文件。
合理设置栈大小。裸机小程序默认 1KB 可能够用,但当你引入大型协议栈、嵌入式 RTOS、FATFS 文件系统后,任务栈是单独分配的,系统栈可能变成中断和异常的专用栈,此时要格外关注栈空间是否充足。偏保守的做法是先用默认配置跑通功能,再根据实际情况调优。
外部晶振和时钟配置要匹配。启动代码中的 SystemInit 会按照system_stm32f10x.c中定义的宏来配置 PLL 倍频系数。如果你的板子外部晶振不是 8MHz,必须修改HSE_VALUE宏,否则串口波特率、定时器定时值全部会偏差。
开启 CRC 校验或看门狗前,先理解启动时序。独立看门狗一旦开启,不能软件关闭,启动过程如果耗时较长,可能导致看门狗在main()之前就溢出复位。所以看门狗初始化要放在main()中尽早的位置,并且喂狗周期要覆盖整个初始化流程。
使用 HAL 库时注意HAL_Init()的调用位置。HAL 库工程中,main()开头会先调用HAL_Init(),它在内部会设置 SysTick、NVIC 分组等。这个函数依赖前面的启动流程提供正确的时钟和栈环境,不要在它之前做复杂外设初始化。
调试时善用断点和寄存器窗口。当怀疑启动过程出现问题时,可以在Reset_Handler、SystemInit、__main处分别设置断点,通过断点是否被命中,快速二分定位问题在哪个阶段。这种排查效率远高于盯着代码反复看。
9. 从启动过程延伸出去的学习路径
启动过程并不是一个孤立的知识点,它和你后续学习的很多内容都有强关联。
如果这次把启动过程理解透了,下一步可以顺着这些方向继续深挖:
- 中断系统:理解向量表以后,学习 NVIC、中断优先级、中断服务函数的响应流程会轻松很多。
- 时钟树:SystemInit 做的事情本质就是顺着时钟树配置各个时钟源和分频系数,这份知识直接决定外设能否正确工作。
- 链接脚本与内存布局:深入理解分散加载文件,能帮你解决很多工程移植问题。
- RTOS 移植:FreeRTOS 等系统移植时,启动文件中关于堆栈的设置、PendSV 和 SysTick 中断向量配置,都和启动过程的知识有关。
- IAP 与 Bootloader:实现固件在线升级时,必须掌握向量表重映射、Flash 读写、引导跳转等能力,这些全都建立在启动过程的基础上。
一次把启动过程学扎实,后续可以省下大量排查底层问题的时间。面试时讲到启动过程,也不只是背答案,而是真正理解芯片从上电到用户代码之间的那段“幕后工作”,这样的理解深度在写代码、查 bug、做方案时都会体现出价值。