ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32上电启动全解析:从复位向量到第一个任务

STM32上电启动全解析:从复位向量到第一个任务

写文章之前,我先说一个个人感受:很多工程师写了几年 STM32,平时工程模板都是 IDE 自动生成的。点一下编译,点一下下载,程序就跑起来了。可真要问他——上电的第一条指令是谁规定的、栈指针最初指向哪里、SystemInit 在 main 之前还是之后执行、RTOS 里的第一个任务是怎么"无中生有"跑起来的——往往就说不利索了。这篇文章就是想把这整条链路捋清楚:从复位向量到第一个任务,STM32 上电启动全过程拆开来看。


1. 上电的那一瞬间:复位向量和启动地址映射

1.1 向量表:一张异常入口的查找表

先说一个最核心的概念:向量表(Vector Table)。

STM32 用的内核是 ARM Cortex-M 系列,这和传统的 ARM7/ARM9 不一样。Cortex-M 上电之后,CPU 不是从固定地址执行一条跳转指令,而是从内存的起始地址连续取两组数据:

  • 地址0x00000000处的数据,写入MSP(主栈指针);
  • 地址0x00000004处的数据,写入PC(程序计数器)。

这个 PC 值,就是复位后第一条指令的地址,也叫做复位向量。

所以向量表本质上是一张"异常和服务跳转表"。第 0 项是初始栈指针,第 1 项是复位向量,第 2 项是 NMI,第 3 项是 HardFault……每一项占 4 字节。这张表不只是上电用,后续每一个中断触发时,CPU 都会根据中断号去这张表里查对应的入口地址。

这就解释了为什么 IDE 生成的工程里,所有启动文件都长得很像:先是__initial_sp,接着一大堆DCD伪指令,后面跟着 Reset_Handler、NMI_Handler、HardFault_Handler……这些DCD排出来的,就是整个向量表。

1.2 BOOT0 和 BOOT1:决定 CPU 从哪读第一条指令

STM32 的启动介质不是写死的,而是由 BOOT 引脚的电平状态决定的。以常用的 F1 系列为例:

BOOT0BOOT1启动介质说明
0任意主 Flash(0x08000000)正常用户程序,绝大多数项目用这种
10系统存储器(0x1FFF0000)出厂自带的 Bootloader,做串口烧录用
11内置 SRAM(0x20000000)调试用,掉电即失,一般量产不用

这里有个关键点:物理地址和启动地址不是一回事。比如从主 Flash 启动时,Flash 既可以在0x08000000访问,同时总线还会把它映射到0x00000000。CPU 在复位后访问0x00000000,取到的其实就是 Flash 最开头的 4 字节。

我曾经踩过一个非常基础的坑:做一块板子的时候,为了省事把 BOOT0 直接接高电平,平时也意识不到问题,直到某天刷完程序发现完全不跑,一查才发现 BOOT0 一直是 1,系统每次都在跑内置 Bootloader。这个场景到现在我都记得,排查了整整一晚上。从那以后,我凡是画板子,BOOT0 必加 10k 下拉电阻,宁可多一个元件,也不赌默认电平。

1.3 地址映射的玄机:为什么 0x00000000 和 0x08000000 都指向 Flash

再往深挖一层,为什么 Flash 会同时出现在两个地址区间?

原因是 Cortex-M 内核的总线设计中,启动地址映射是可配置的。芯片上电后,由 BOOT 引脚选定的启动介质会通过系统总线映射到别名区0x00000000。而 Flash 本身的物理地址0x08000000依然有效,所以程序员既可以按物理地址操作 Flash,CPU 又可以从映射区读启动向量。

这带来一个实际影响:如果你在代码里修改了向量表偏移(SCB->VTOR),比如做 Bootloader 跳转 App 时把 VTOR 改到了0x08008000,那么后续所有中断都会到新地址去查表,而复位时的初始取向量只认0x00000000映射区的第一份。所以 Bootloader 跳转时必须确保 App 的起始地址处同样放着一张合法向量表,否则一进 App 就 HardFault。类似的"程序跑飞"事故,多半就是跳转地址写错或者中断向量表没设置好。

2. 启动文件:被 IDE 悄悄铺好的路

2.1 栈和堆:C 语言运行环境的第一块地基

拿到一个标准库工程,启动文件里第一眼看到的通常就是栈和堆的定义:

Stack_Size EQU 0x00000400 AREA STACK, NOINIT, READWRITE, ALIGN=3 Stack_Mem SPACE Stack_Size __initial_sp Heap_Size EQU 0x00000200 AREA HEAP, NOINIT, READWRITE, ALIGN=3 __heap_base

这两段内容看似平平无奇,但它们决定了你程序能不能跑得稳:

  • 栈(Stack):存放函数调用时的返回地址、局部变量、压栈现场。栈是从高地址向下生长的,__initial_sp就是栈顶。复位后 MSP 指向这里,之后每次函数调用、中断压栈,都用这块区域。
  • 堆(Heap):给malloc用的动态内存区。如果项目里不用malloc,堆大小设 0 都可以;但如果用了第三方库或者 RTOS 的某些内存分配功能,堆太小会导致运行期才爆出来的内存分配失败。

关于栈大小的选择,我个人的标准做法是:裸机工程默认0x400(1KB)够用,但一旦加了浮点 printf、递归函数或较深的中断嵌套,保险起见直接上0x800甚至0x1000。RTOS 工程则不看启动文件里的栈,因为任务栈由 RTOS 自己管理,启动文件里那个栈只是给启动阶段和 main 用的。

至于为什么这里__initial_sp必须放在 STACK 段末尾而不是开头?因为栈向下生长,栈顶地址才是 MSP 的初始值,这个数值恰好是 STACK 区域的最高地址。

2.2 中断向量表的数据结构

启动文件的中段,是一张长长的手工列表,开头像这样:

AREA RESET, DATA, READONLY EXPORT __Vectors EXPORT __Vectors_End EXPORT __Vectors_Size __Vectors DCD __initial_sp ; Top of Stack DCD Reset_Handler ; Reset Handler DCD NMI_Handler ; NMI Handler DCD HardFault_Handler ; Hard Fault Handler DCD MemManage_Handler ; MPU Fault Handler DCD BusFault_Handler ; Bus Fault Handler DCD UsageFault_Handler ; Usage Fault Handler DCD 0 ; Reserved ...

DCD本质上是定义一个占 4 字节的数据项。__Vectors标号是这张表的起始地址,链接后默认就在0x08000000。第 0 项是栈顶,第 1 项是复位处理函数地址。

这里有一个新手很容易忽略的细节:地址必须是"偶数对齐到 4 字节"的。Cortex-M 读取向量表时要求字对齐,如果链接脚本或者自定义的启动区段没对齐,复位后取 PC 值可能错位,程序直接跑飞。

另外,F1 系列的表项数量不会太多,一般几十项;F4/H7 的向量表更长,因为中断源更多。H7 系列还多了一组"系统异常"和"外部中断"的分段设计,但核心逻辑是一样的。当你看到芯片多了新的外设中断时,本质就是在向量表的尾部追加新的DCD项。

2.3 Reset_Handler 到底干了什么

复位向量指向的是Reset_Handler,这是整个 C 运行环境启动前最重要的一段汇编。

下面是常见启动文件里 Reset_Handler 的完整逻辑(以标准外设库的 startup_stm32f10x_hd.s 为例):

Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT SystemInit IMPORT __main LDR R0, =SystemInit BLX R0 LDR R0, =__main BX R0 ENDP

大致流程:先调用SystemInit,然后跳到链接器生成的__main,由__main完成 C 运行时环境的初始化后,再自动调用用户写的main函数。注意启动文件里的main是 C 库函数__main,和用户代码里的main不是同一个符号。

如果是用 GCC 工具链(比如 STM32CubeIDE 生成的工程),启动文件里对应的往往是:

bl SystemInit bl main bx lr

少了__main,而是直接在汇编里调用main。所以不同工具链对"进入 main 之前"的处理不同,但系统时钟初始化的责任是一致的:SystemInit 必须在 main 之前执行,否则 main 里一上来就配置外设,可能仅因为跑的还是 8MHz 内部 RC,各种时序全都不对。

这段汇编虽然只有四五行,却藏着启动顺序的全部框架。可以说,理解了 Reset_Handler,就理解了 STM32 启动流程的一半。

3. SystemInit 和时钟树:为什么开机要先锁时钟

3.1 复位后默认使用的 HSI

芯片复位后,不是立刻就跑到 72MHz 或 168MHz 的。Cortex-M 内核上电后所有时钟都处于最保守的状态:

  • 时钟源是HSI(高速内部 RC 振荡器),频率约 8MHz;
  • PLL(锁相环)未使能;
  • AHB/APB 分频器全部是默认值,也就是不分频。

也就是说,你按下复位键的那一刻,CPU 实际上在以 8MHz 甚至更低的速度运行。如果这时候你不配置 PLL,不切换系统时钟源,程序也能跑,但外设定时、波特率计算、PWM 频率全部会按 8MHz 来算,出来的结果自然各种不对。

我在教新人排查串口乱码时,最常说的一句话就是:"先去看你的时钟树有没有配成外部晶振,再看波特率。" 大部分串口乱码问题的根源不在波特率公式,而是系统时钟根本没达到你想象中的主频,导致分频后的波特率实际值和理论值差了近十倍。

3.2 SystemInit 里的三项关键配置

以 F1 为例,SystemInit 做的事情大致分三步(简化描述,实际代码比这复杂,涉及 RCC_CR/RCC_CFGR/RCC_CFGR2 等一堆寄存器):

  1. 设置 Flash 等待周期:主频越高,Flash 读取需要插入的等待周期就越多。F1 在 72MHz 时通常设 2 个等待周期。这一步如果不做,Flash 跟不上 CPU 速度,程序会随机跑飞。
  2. 配置 HSE 并使能:打开外部高速晶振(HSE),等待它起振稳定。HSE 起振需要时间,所以while等待标志位是必须的。
  3. 配置 PLL 倍频并切换系统时钟源:把 PLL 的倍频系数设好(比如 HSE 8MHz × 9 = 72MHz),使能 PLL,等它锁定后把系统时钟从 HSI 切换到 PLL。
/* Set Flash Latency to 2 wait states */ FLASH->ACR |= FLASH_ACR_LATENCY_2; /* HCLK = SYSCLK */ RCC->CFGR |= RCC_CFGR_HPRE_DIV1; /* Enable HSE */ RCC->CR |= ((uint32_t)RCC_CR_HSEON); while (!(RCC->CR & RCC_CR_HSERDY)) { /* waiting for HSE ready */ } /* Configure PLL: HSE / 1 * 9 = 72 MHz */ RCC->CFGR |= RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9; /* Enable PLL */ RCC->CR |= RCC_CR_PLLON; while (!(RCC->CR & RCC_CR_PLLRDY)) { /* waiting for PLL ready */ } /* Switch system clock to PLL */ RCC->CFGR &= ~RCC_CFGR_SW; RCC->CFGR |= RCC_CFGR_SW_PLL; while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) { /* waiting for PLL as system clock source */ }

如果是 H7 系列,这个过程会更长,因为 H7 有双精度浮点单元、AXI SRAM、多组时钟域,而且复位后 SYSCLK 只有 64MHz(相当于 HSI 被二分频)。H7 的 SystemInit 里还会做 SRAM 的 ECC 初始化。这也是为什么 H7 项目启动时如果加了 ECC 错误检查,会比 F1 慢一小截的原因。

3.3 主频配错或起振失败的典型症状

系统时钟配置错了,往往不会在启动时当场崩溃,而是表现为各种"看起来毫无规律"的故障。我总结下来,最常见的三种:

故障一:延时函数卡死。比如 SysTick 定时器配置成 HCLK/8,结果 HSE 一直没起振、程序卡在 SystemInit 的 while 死循环里,或者 PLL 没锁定就往下走,后面所有用到 SysTick 的 delay 全都不跑。表现为程序一上电就"卡死",断点却始终停在时钟等待循环里。

故障二:串口波特率对不上。外部晶振是 8MHz,配置里却按 25MHz 算,PLL 倍频后系统时钟差出一大截,波特率自然全错。这种情况第一步查 HSE_VALUE 宏,第二步查 PLL 倍频系数。

故障三:Flash 等待周期不对导致偶尔 HardFault。把主频超到 128MHz,Flash 等待周期还是 2 或者忘记改,读指令时时序跟不上,程序在随机位置进入 HardFault。这种最恶心,因为不是必现,跟温度、电压都有关系。

所以每次新建工程,我都会在 SystemInit 的最后加两行调试用的断点条件,或者在 main 开头读 RCC->CFGR 和 RCC->CR 寄存器,先确认时钟树状态再往下走。别嫌麻烦,这能省掉后面几天排查时间。

4. 从 Reset_Handler 到 main:C 运行环境的建立

4.1 __main 与分散加载

当 SystemInit 返回后,Reset_Handler 会把控制权交给__main。这块是 ARM C 库(或者 GCC 的 crt 启动代码)在背后做的事,主要分两件:

  • 分散加载:把存储在 Flash 里的 RW(已初始化全局变量)段拷贝到 RAM,并填写 ZI(零初始化)段为 0。
  • 调用main:准备好 C 运行环境后,调用用户的main入口。

为什么不能直接把全局变量存在 Flash 里用?因为 Flash 是只读的,而程序运行过程中会初始化、修改全局变量。比如:

uint32_t counter = 100;

100这个值在编译后存放在 Flash 的 RO/RW 段里。程序启动时,__main把这段数据从 Flash 拷到 RAM,之后counter的读写都发生在 RAM 地址上。

所以,如果你用调试器在 Reset_Handler 打断点单步,会发现跳到__main之后的头几十条指令就是在做内存拷贝和清零。这个过程看起来没什么技术含量,但一旦你在启动早期就访问了尚未拷完的全局变量,读到的就是 0 或者随机值。这也是为什么主程序对全局变量初始化的依赖,不能放到外设初始化之前。

4.2 进入 main 之前的栈与堆检查

ARM 的 C 库在进入 main 之前还会做一件事:检查栈指针和堆指针是否越界(取决于库配置)。有些库函数比如malloc内部使用__heap_base和__heap_limit来划定堆边界。如果堆和栈没有在链接脚本里划分清晰,或者重叠了,malloc 可能返回超出边界的地址,后续写数据直接覆盖函数栈,必然 HardFault。

提示:当你的程序"莫名奇妙地在 main 刚开始就 HardFault",第一件事不是怀疑外设配置,而是查启动文件里的 Stack_Size 和 Heap_Size。把两个都调大一档,再来一遍,往往就过了。

还有一件容易被忽略的事:启动文件里的栈大小是给"启动阶段"用的,不是给任务用的。如果是 RTOS 工程,这个栈会被移植代码在启动结束后暂时借用,或者干脆闲置。但不管用不用,__initial_sp这个值必须存在且有效——它决定了复位后 CPU 有没有一个合法的栈可用。

4.3 裸机 main:你的第一个"任务"

当__main完成初始化,真正调用到main函数的一瞬间,芯片才从"引导阶段"进入"用户程序阶段"。

在裸机场景下,main 就是你的第一个任务。它通常长这样:

int main(void) { SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); while (1) { // 主循环调度 } }

这里有一个非常形象的类比:整个启动流程,就像一场接力赛。复位向量把 SP/PC 交到 Reset_Handler 手里,Reset_Handler 把系统时钟交到 SystemInit,C 库把运行环境交到 main,而 main 里的while(1),就是最后接棒的那个选手——它必须永远跑下去,因为一旦 main 返回,C 库会调用exit,程序就"死"了。

在裸机中,所谓"任务"其实就是一个无限循环,加上被中断驱动的各种状态机。你写的每个功能模块,都是在 main 的 while(1) 里被轮询或者被中断打断执行。所以在裸机语境里,第一个任务不一定非要有 RTOS 的任务控制块,只要它在 main 里开始了持续循环,它就是启动流程跑完后的第一个"常驻工作单元"。

我经常把裸机主循环理解为"一个永不结束的函数,和一个不断被打断的线程"——中断就是最朴素的并发。理解了这一点,再去接触 RTOS 的任务上下文切换,会顺畅得多。

5. 当 RTOS 介入:第一个任务是怎么被调度起来的

5.1 RTOS 启动的最后一跳

如果你用的是 FreeRTOS、uC/OS-III 这类 RTOS,main 里通常是这样:

int main(void) { SystemClock_Config(); MX_GPIO_Init(); xTaskCreate(AppTask, "AppTask", 512, NULL, 1, &AppTaskHandle); vTaskStartScheduler(); /* 这一步之后,运行权永久交给调度器 */ while (1); }

xTaskCreate创建任务时,RTOS 内部会为每个任务分配一个任务栈和 TCB。任务栈里预先放置了一份"伪造的上下文"——包含初始 PC(任务函数入口)、xPSR、返回地址、寄存器初值。这一步是整个 RTOS 启动最精妙的地方:它让任务看起来好像曾经被中断过,只是还没恢复现场。

vTaskStartScheduler()之后,调度器开始运行。它会先从待运行链表里挑出最高优先级任务,然后触发一次 PendSV 中断,在 PendSV 里做上下文切换,把当前控制权交给下一个任务的"伪造现场"。

那次切换完成时,CPU 看到的画面是什么样的?

  • 任务栈指针(PSP)指向任务 TCB 中存放的上下文区域;
  • 从内存里弹出 xPSR、PC、LR、R0-R12;
  • 程序流跳转到任务函数的入口;
  • 从此之后,这个任务跟"刚被中断过又继续运行"一样,从函数开头开始执行。

所以 RTOS 里所谓"第一个任务",本质上就是调度器把一份精心伪造的初始上下文恢复到了一个任务栈上,然后从那个入口地址开始运行。

5.2 用 SVC 和 PendSV 完成上下文切换

Cortex-M 为 RTOS 提供了两个利器:SVC(Supervisor Call)和 PendSV(Pendable Service Request)。

  • 启动调度器时,vTaskStartScheduler会触发一次 SVC,进入特权模式,设置好线程模式使用 PSP;
  • 平时任何任务切换请求,都会置位 PendSV 的中断挂起位;
  • PendSV 的优先级被设置为最低,这样中断可以打断任务切换,避免在中断处理期间切换上下文造成冲突。

看起来高深,其实核心动作不过是:保存当前任务的寄存器组到它的 TCB,再从新任务的 TCB 里恢复寄存器组。由于任务上下文的第一份数据是"伪造"的,第一次切换等价于完成了"从调度器跳进任务函数"这个动作。

提示:在 FreeRTOS 的移植文件 port.c 里搜索vPortStartFirstTask或者SVC,你能看到启动第一个任务的汇编代码。核心就十几行:设置 PSP、恢复上下文、bx r14。看懂这十几行,整个 RTOS 的任务调度机制就通了。

5.3 为什么常听说"第一个任务必须是入口函数"

不少 RTOS 教程里会强调"第一个被创建的任务,不能返回,必须是死循环"。这个说法的原因很简单:如果任务函数return了,RTOS 会把它当作任务结束,并调用钩子函数或直接停掉该任务。在有些老版本移植里,任务函数返回后 PC 会跳到不可预期的地址,系统当场崩溃。

所以正规做法是:

static void AppTask(void *argument) { /* 初始化 */ for (;;) { /* 任务主体 */ } }

如果确实需要任务退出,也应该显式调用vTaskDelete(NULL),而不是让函数自然返回。这和裸机 main 不许 return 是同一个道理——启动流程建立的运行环境,要求最后一个接管者永不交权。

理解了这一点再看整个 RTOS 启动:复位向量的接力棒,经过 SystemInit、C 运行时初始化、main、调度器,最终落到第一个任务的 while(1) 里,整条链路才算闭环。

6. 用调试器亲眼验证启动流程

6.1 三板斧:核心寄存器窗口、断点、Reset and Run

理论说再多,都不如直接在调试器里看一遍启动流程。我平时最常用的三板斧:

第一板斧:看核心寄存器。在 Keil 的 Registers 窗口里,复位后关注 SP 和 PC。刚复位时,SP 应该等于__initial_sp,也就是启动文件里定义的栈顶地址(比如0x20000400)。PC 则指向 Flash 起始区段附近的Reset_Handler。如果 SP 不是栈顶值,大概率是芯片没有真正从 Flash 启动,检查 BOOT0。

第二板斧:在关键点打断点。我会在以下位置各放一个断点,然后按重启:

  • Reset_Handler的第一行指令;
  • SystemInit内第一行(或者直接断在RCC->CR |= RCC_CR_HSEON附近,确认 HSE 起振);
  • main函数的第一行;
  • 如果是 RTOS,再断在vTaskStartScheduler()内部或第一个任务的入口函数。

单步跑一遍,观察 PC 的跳转路径,你就完全能"看见"启动顺序了。我见过有朋友说"程序死在 Reset_Handler 里",结果断点一看,其实是在 SystemInit 的 while 等待循环里死等 HSE 标志位——外部晶振没焊好或者起振电容不匹配,这是最常见的启动卡死原因。

第三板斧:Reset and Run。下载完程序后,TroOK 工具(Keil)里选 Settings -> Debug,勾上 "Reset and Run"。这样每次烧录后芯片会自动重跑启动流程,省去手动按复位的步骤。但如果勾上之后程序还是一动不动,那大概率是启动阶段就故障了,需要回到断点单步排查。

6.2 断点打断点表的技巧

有一个调试技巧我觉得特别值得分享,虽然标题叫"断点打断点表"有点绕,但实际很实用。

在某个中断服务函数第一行打断点,其实是把断点覆盖在那个中断向量的入口地址上。Cortex-M 支持硬件断点数量有限(F1 一般是 6 个),如果你同时想观察多个中断入口,数量可能不够。这时可以把断点下在向量表所在的地址上看一下对应的 DCD 值,或者直接在启动文件里临时把某个中断向量改成你的调试函数,等验证完再改回来。

还有一个更早的"被动断点"方式:如果程序死在 HardFault_Handler 里,先在 HardFault_Handler 第一行打断点,然后打开 Call Stack 窗口,查看压栈的 PC/LR 地址,再反汇编这个地址,就能定位到触发硬错误的那条指令。绝大多数启动期 HardFault 都可以用这个流程锁定根因,不需要瞎猜。

6.3 启动流程故障排查速查表

结合我这些年带项目的经验,把启动阶段常见的故障现象、根因和排查方向整理成一张表:

故障现象最常见根因排查手段
上电后完全不运行BOOT0 引脚电平不对,没从 Flash 启动万用表量 BOOT0 电压;检查外部下拉电阻
调试器连接不上SWD 引脚被复用成 GPIO / 芯片进了低功耗 / 读保护被打开尝试 Connect under Reset;如果不行,设 BOOT0=1 从系统存储器启动后用工具连上并擦除
永远停在初始化 while 循环HSE 起振失败 / PLL 锁定失败查晶振、负载电容、RCC 配置;断点在 SystemInit 的 while 上观察 CR 寄存器
进 main 后短暂运行就 HardFaultFlash 等待周期不对 / 栈溢出 / 全局变量未拷完就被访问查 PLL 后主频 vs Flash 等待周期;加大 Stack_Size 重试;检查链接脚本
外设时间全不对时钟源没切到 PLL,实际跑在 8MHz HSI读 RCC->CFGR 的 SWS 位,确认系统时钟源
烧录成功但任务没创建RTOS 堆内存不足 / 任务栈过大检查 FreeRTOSConfig.h 的 heap 大小,调大 heap 后重试
启动顺序乱了链接脚本向量表偏移或分散加载文件配置异常检查 .icf/.ld 文件,确认向量表地址和应用起始地址一致

这里特别提一下"调试器连接不上"这个场景。很多人遇到 SWD 无法连接就直接慌了,其实最常见的两种情况:一是程序把 PA13/PA14(SWDIO/SWCLK)复用成了普通 IO,导致调试口被占用;二是芯片设置了读保护(RDP 级别 1),调试器默认连不上。

解决思路是先用 BOOT0=1 强制从系统存储器启动(绕过用户程序),用 STM32 ST-LINK Utility 连接后执行"全擦除",清掉读保护和占用 SWD 的用户程序,然后再把 BOOT0 拉回 0,重新烧录。这个过程不会破坏芯片,只是把用户区擦掉而已。


我个人实际带项目的体会是:启动流程这东西,越早花时间彻底搞明白,后面省的时间越多。你不一定非要记住每个寄存器的每一位,但一定要有清晰的流程图——复位向量在哪、SP/PC 谁定的、SystemInit 做了什么、C 库怎么铺路、第一个任务怎么接管。这个框架一旦建立,遇到任何"上电异常"类的问题,你的排查起点都是明确的,而不是拿着一堆工程文件瞎猜。最后再分享一个小技巧,每次新建工程或者换芯片型号,我都会把启动文件从头到尾通读一遍,再在调试器里单步走一遍启动流程,顺手截屏保存。这个习惯坚持了几年,几乎再也没有被"上电不启动"这种问题折磨过。

返回列表