☰
STM32启动流程深度解析:从复位向量到main函数的完整指南
2026/10/13 16:18:50 网站建设 项目流程

1. 为什么一定要搞懂启动流程

很多刚接触 STM32 的开发者都遇到过类似场景:代码编译零错误、烧录提示成功,但板子就是没反应;或者换了一颗芯片,程序跑起来就进 HardFault;再或者自己写的 bootloader 跳转 App 后,中断全部失灵。表面上看是“硬件问题”“芯片问题”,其实大多数情况都是对启动流程的理解不够。

无论你是用标准外设库、HAL 库还是直接操作寄存器,无论是开发裸机程序还是基于 FreeRTOS、RT-Thread 做嵌入式项目,startup文件(启动文件)、链接脚本和SystemInit这三样东西,决定了芯片从上电复位到进入main函数之前的所有行为。本文就以 STM32 为硬件基础,以开源嵌入式项目 Flipper Zero 的实战风格为参考,把启动流程拆开讲透,并配上一个完整的“最小工程 + 启动分析 + 调试排查”实操教程。

本文适合以下读者:

  • 已经会用 Keil 或 STM32CubeIDE 点灯,但还不清楚芯片内部到底发生了什么;
  • 想自己移植 STM32 固件库、搭建工程模板的初学者;
  • 编写 bootloader 或做固件跳转时,经常遇到中断异常问题的进阶开发者;
  • 准备嵌入式面试,需要系统梳理“STM32 启动流程”知识点的求职者。

学完本文,你应该能回答这几个问题:

  1. STM32 复位后,CPU 是从哪里取得第一条指令的?
  2. 启动文件里那几百行汇编到底做了什么?
  3. SystemInit和SystemClock_Config有什么区别?
  4. 从复位到main,数据段和 BSS 段是谁帮忙处理的?
  5. 程序跑飞、进 HardFault、中断不响应时,如何通过启动流程定位问题?

2. 启动流程的核心概念:复位向量与中断向量表

2.1 上电后芯片在干什么

STM32 属于 Cortex-M 系列内核。Cortex-M 内核设计了一种“向量表驱动”的启动方式,和传统 51 单片机(从地址 0 直接开始执行)完全不同。

当 STM32 上电或按下复位键后,芯片内部硬件会自动完成以下几个动作:

  1. 从存储器的0x00000000地址处读出初始栈顶地址(MSP 值)。
  2. 从0x00000004地址处读出复位中断向量(Reset_Handler 的地址)。
  3. 将读到的栈顶地址写入主堆栈指针 MSP。
  4. 将读到的复位向量写入程序计数器 PC。
  5. 跳转到 Reset_Handler 开始执行。

也就是说,0x00000000这个地址在 Cortex-M 中并不是第一条指令,而是栈指针;0x00000004才是复位后要执行的函数入口的地址。这一点是理解整个启动流程的地基。

用表格对比更直观:

地址Cortex-M 内容51 单片机
0x00000000初始栈顶地址 MSP第一条可执行指令
0x00000004复位向量 Reset_Handler第二条指令
0x00000008NMI 异常入口普通指令
0x0000000CHardFault 异常入口普通指令

2.2 三种启动方式

STM32 有 BOOT0、BOOT1(部分型号是 BOOT0 一个引脚,加上 Option Bytes)引脚,用于选择复位后的启动存储区。不同芯片型号引脚略有差异,但思想一致。

常见启动方式如下:

BOOT0BOOT1启动区域典型用途
0x主 Flash正常运行用户程序
10系统存储器使用内置 BootLoader 进行串口 ISP 下载
11SRAM调试启动,掉电丢失

在实际开发板上,BOOT0 通常会通过跳线帽或拨码开关引出。程序下载后“不运行”,第一个要检查的就是 BOOT0 是否正确接地(选择从主 Flash 启动)。

2.3 向量表偏移:一个容易踩坑的地方

在启动流程中,向量表默认放在 Flash 起始地址。但是当我们做 bootloader + app 架构时,App 程序往往放在0x08008000之类的偏移地址。此时如果不修改向量表偏移量,App 中一旦发生中断,CPU 仍然会去0x08000000找中断入口,就会导致中断不响应或进入 HardFault。

Cortex-M 提供了SCB->VTOR寄存器来设置向量表偏移。在支持该寄存器的型号中,需要这样设置:

#define APP_ADDRESS 0x08008000U SCB->VTOR = APP_ADDRESS;

这个操作通常放在 App 的最早期,甚至在进入main之前就可以完成。如果你是在做嵌入式项目二次开发,不理解这一行代码,就很容易在 bootloader 跳转后栽跟头。

3. 环境准备与工程结构

在深入代码之前,先准备好一套可复现的实验环境。本文的示例以常见的 STM32F103C8T6 开发板(所谓“蓝丸”)为例,整套思路同样适用于 Flipper Zero 使用的 STM32WB55 系列,也适用于 STM32F4、F7 系列。

3.1 开发工具链

  • Keil MDK 或 STM32CubeIDE,两者任选其一。建议新手先用 Keil,资料多,Debug 配置直观。
  • STM32CubeMX:用于快速生成工程和启动文件,也可以用官方固件库手动搭建。
  • ST-Link 或者 J-Link:用于烧录和在线调试。
  • 一个 LED 最小电路或者直接使用开发板上的板载 LED。

版本说明:

STM32CubeMX、Keil MDK 以及固件包版本更新较快,具体版本号请以你下载时的最新稳定版为准。本文示例不依赖于特定版本,重点展示启动流程的分析思路和代码定位方法。

3.2 示例工程结构

使用 STM32CubeMX 生成最小工程后,核心文件结构如下:

MyProject/ ├── Core/ │ ├── Inc/ // 头文件目录 │ │ ├── main.h │ │ └── stm32f1xx_hal_conf.h │ └── Src/ │ ├── main.c // 用户代码入口 │ ├── stm32f1xx_hal_msp.c │ └── system_stm32f1xx.c ├── Drivers/ │ ├── CMSIS/ // 内核相关定义 │ └── STM32F1xx_HAL_Driver/ // HAL 库源码 ├── MDK-ARM/ │ ├── startup_stm32f103xb.s // 启动文件 │ ├── MyProject.uvprojx // Keil 工程文件 │ └── ... └── MyProject.ioc // CubeMX 工程文件

其中:

  • startup_stm32f103xb.s是启动文件,也是本文分析的主角。
  • system_stm32f1xx.c提供了SystemInit函数。
  • main.c是用户代码入口。
  • 链接脚本(Keil 中自动生成,GCC 工程为.ld文件)告诉链接器代码段、数据段放到哪里。

4. 启动文件逐段拆解

现在打开启动文件startup_stm32f103xb.s。汇编代码看起来有点劝退,但它做的事情非常固定,拆成几块就很好懂了。

4.1 栈和堆的分配

文件开头会定义栈和堆的大小:

Stack_Size EQU 0x400 AREA STACK, NOINIT, READWRITE, ALIGN=3 Stack_Mem SPACE Stack_Size __initial_sp Heap_Size EQU 0x200 AREA HEAP, NOINIT, READWRITE, ALIGN=3 __heap_base Heap_Mem SPACE Heap_Size __heap_limit

这里做的事情是:

  1. EQU定义栈大小为 0x400(1KB)。
  2. 在STACK段中开辟一块内存区域。
  3. __initial_sp是栈顶地址,最终会被放到中断向量表的第一个位置。
  4. __heap_base和__heap_limit用于 C 库的动态内存分配。

为什么栈大小很重要?

栈(Stack)用于函数调用、局部变量、中断现场保存。如果你在中断里声明一个大数组,或者在函数里用递归,栈空间不足就会导致栈溢出,程序跑飞。在实际生产项目中,建议把栈设为至少 1KB,如果调用层级深或中断嵌套多,应适当加大到 2KB、4KB。但有 RTOS 时,任务栈一般是单独分配的,启动文件里的栈通常只给启动阶段和中断主栈使用。

4.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 0 ; Reserved DCD 0 ; Reserved DCD 0 ; Reserved DCD SVC_Handler ; SVCall Handler DCD DebugMon_Handler ; Debug Monitor Handler DCD 0 ; Reserved DCD PendSV_Handler ; PendSV Handler DCD SysTick_Handler ; SysTick Handler ; 外部中断向量省略

汇编指令DCD的含义是“定义一个 32 位数据”。所以这段代码本质上是把函数地址按固定顺序填到一块连续内存中,这个内存区域的名字叫__Vectors。

向量表第一个元素是__initial_sp,对应0x00000000;第二个元素是Reset_Handler,对应0x00000004。后面的每一个元素都对应一种异常或中断的入口地址。

要注意的是:

  • 中断向量表的地址顺序是内核和芯片厂商约定好的,不能随意调整。
  • 向量表里的每一项占 4 字节,所以__Vectors_Size可以计算出整个表的大小。
  • 用户中断(比如TIM2_IRQHandler、USART1_IRQHandler)在文件后面继续追加。

4.3 Reset_Handler:一切从这里开始

向量定义完之后,就进入最主要的代码段:

AREA |.text|, CODE, READONLY Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT SystemInit IMPORT __main LDR R0, =SystemInit BLX R0 LDR R0, =__main BX R0 ENDP

这段汇编翻译成 C 语言的伪代码是这样的:

void Reset_Handler(void) { SystemInit(); // 初始化时钟、复位 RCC 寄存器等 __main(); // C 库入口,负责数据段拷贝和 BSS 清零 }

具体流程分析:

  1. LDR R0, =SystemInit把SystemInit函数的地址加载到 R0。
  2. BLX R0带链接跳转,调用SystemInit。
  3. 返回后继续执行,LDR R0, =__main。
  4. BX R0跳转到__main。

__main是 ARM 编译器 C 库的入口,它最终会调用main。如果使用 GCC 工具链,名字通常叫_start。它负责以下操作:

  1. 把只读数据区的初始值拷贝到可读写的 RAM 中(RW 段)。
  2. 把未初始化数据段清零(ZI 段,也就是 BSS)。
  3. 初始化堆栈和 C 库环境。
  4. 调用main函数。

这就是为什么启动文件没有main却能“跑到”你的 C 代码的底层原因。

4.4 中断服务函数的弱定义

除了 Reset_Handler,启动文件后面还会给每个中断定义一个“弱引用”处理函数:

NMI_Handler PROC EXPORT NMI_Handler [WEAK] B . ENDP

B .是一条死循环指令。[WEAK]的意思是:如果你在 C 代码里重新定义了一个同名函数,链接器优先使用你的强定义;如果你没有定义,默认执行这个死循环。

这样设计的好处是:即使你开启了一个中断但没有写对应中断服务函数,程序不会产生链接错误。但坏处也很明显——如果忘记写中断服务函数,中断触发后程序会陷入死循环,看起来就像是“程序卡死”。

5. SystemInit 与时钟树配置

5.1 SystemInit 的职责

Reset_Handler调用的SystemInit在system_stm32f1xx.c中。它的主要职责是:

  1. 将 RCC 相关寄存器复位到默认值。
  2. 设置 Flash 等待周期。
  3. 默认选择 HSI 作为系统时钟。
  4. 关闭 PLL。

注意一个容易混淆的点:

  • SystemInit只会把系统时钟配置成芯片上电后的“安全默认状态”,通常是 HSI 8MHz。
  • 真正把系统时钟提升到 72MHz、配置 PLL、配置 HSE 的代码,在 HAL 库中是SystemClock_Config,由 CubeMX 生成,写在main.c中,并在main函数里调用。

简单来说:SystemInit保证芯片能跑,SystemClock_Config才让芯片跑得够快。

5.2 常见时钟配置流程

下面是 STM32CubeMX 生成的SystemClock_Config典型代码(STM32F103 72MHz 配置):

void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) { Error_Handler(); } }

关键点解释:

  • 8MHz 外部晶振通过 PLL 倍频 9 倍,得到 72MHz 系统时钟。
  • APB1 分频 2,所以定时器等外设时钟是 36MHz。
  • APB2 不分频,所以 ADC、USART1 等外设可以跑 72MHz。
  • 如果外部晶振焊接松动或不起振,HAL_RCC_OscConfig会返回错误,直接卡在Error_Handler。

这也是一个非常常见的启动问题:程序下载后没反应,在线调试发现停在Error_Handler,最终查出来是 HSE 晶振没有起振。解决方法是把时钟源改为 HSI,或者修复晶振电路。

6. 从复位到 main 的完整实战:最小工程启动验证

下面用一个最小工程,把前面讲的流程完整走一遍。实验目标是:新建一个 STM32F103C8T6 工程,通过 Debug 模式和断点观察启动流程的执行顺序,并在 LED 上看到现象。

6.1 创建项目结构

打开 STM32CubeMX,新建工程并选择芯片STM32F103C8Tx。在System Core -> RCC中,将HSE设为Crystal/Ceramic Resonator。

在Pinout & Configuration中,将 PC13 引脚配置为 GPIO_Output(蓝丸板载 LED 通常接 PC13,低电平点亮)。

然后在Project Manager中:

  • Project Name 填写stm32_startup_demo。
  • Toolchain 选择 MDK-ARM。
  • Minimum Heap Size 和 Minimum Stack Size 可以保持默认。

点击GENERATE CODE后,CubeMX 会自动生成包含启动文件的 Keil 工程。

6.2 添加核心测试代码

打开生成的main.c,在用户代码区域内加入 LED 闪烁逻辑:

int main(void) { HAL_Init(); SystemClock_Config(); __HAL_RCC_GPIOC_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_13; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); while (1) { HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); HAL_Delay(500); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); HAL_Delay(500); } }

这段代码的作用是让 PC13 上的 LED 以 500ms 间隔翻转。

6.3 用 Debug 模式观察启动流程

在 Keil 中点击 “Debug” 按钮,进入仿真调试界面。然后在启动文件startup_stm32f103xb.s中找到这几处并打上断点:

  1. Reset_Handler的入口处。
  2. LDR R0, =SystemInit这一行。
  3. LDR R0, =__main这一行。

按 F10 单步执行,你会依次看到:

  • 程序停在Reset_Handler。
  • 进入SystemInit,执行完 RCC 寄存器复位。
  • 跳转进入__main,由 C 库完成 RW 段拷贝、ZI 段清零。
  • 最终停在main函数的第一行。

如果你在SystemInit里打断点,会发现它的确是在进入main之前被自动调用的,这就是“启动流程”在真正的硬件上运行的样子。

6.4 验证向量表第一个元素

在 Debug 模式下,打开 Memory 窗口,输入地址0x08000000,你会看到第一行数据是栈顶地址,第二行是 Reset_Handler 的地址。用 Keil 的 Register 窗口查看 SP 寄存器的值,返回的恰好就是0x08000000内存处存放的初始值。

这个操作能直观验证前面说的“0x00000000存的是 MSP 初始值”这一结论。

6.5 运行结果说明

点击全速运行后,LED 应该以 1Hz 频率闪烁(亮 500ms、灭 500ms)。如果 LED 不闪烁,优先检查:

  • BOOT0 是否接地。
  • PC13 是否接的是 LED 正极或负极方向是否正确。
  • 程序是否卡在SystemClock_Config的Error_Handler。

7. 常见问题与排查思路

实际嵌入式项目里,启动相关的问题往往不只是“点不亮 LED”这么简单。下面整理一份高频问题清单,按排查顺序列出来。

问题现象可能原因排查步骤与解决思路
下载程序后完全不运行BOOT 引脚配置错误将 BOOT0 置低,选择从主 Flash 启动;检查复位电路
程序运行但时钟不对外部晶振未起振或焊接不良在线调试看是否停在 Error_Handler,用示波器测 OSC_IN/OSC_OUT 引脚
启动后立刻进 HardFault中断向量表偏移未设置在 App 入口设置SCB->VTOR,并确保向量表 256 字节对齐
中断不响应中断服务函数未实现检查中断服务函数名和启动文件中的向量名是否一致
进入__main前卡死栈配置过小或堆初始值异常在启动文件里把 Stack_Size 调大;检查是否有 C 库初始化依赖
修改中断服务函数后无效在中断向量表中手动新增了错误项用 CubeMX 重新生成工程,避免手工维护向量表
使用 RTOS 后频繁 HardFaultMSP 栈空间不足增大启动文件中的 Stack_Size,或检查 RTOS 是否要求额外预留栈

7.1 如何快速定位启动阶段卡死的位置

在线调试时,程序全速运行后暂停,查看当前 PC 指针:

  • PC 停留在Reset_Handler:说明复位后程序还没进去,可能是 Flash 里没有有效代码。
  • PC 停留在HardFault_Handler:说明在执行过程中发生了非法访问、未对齐访问或错误的异常。
  • PC 停留在Error_Handler:通常是某个外设初始化失败,常见是HAL_RCC_OscConfig返回错误。
  • PC 停留在某个中断的B .死循环:说明该中断触发,但没有实现对应服务函数。

7.2 用启动文件排查 HardFault

进入 HardFault 后,可以通过 Call Stack 窗口查看是从哪个函数跳进去的。如果没有 Call Stack 信息,可以用以下方法:

  1. 打开 Fault Reports 窗口(Keil 5 及以后版本自带)。
  2. 查看入栈的 PC 值。
  3. 结合 Map 文件确认这个 PC 地址属于哪个函数。

如果栈已经被破坏,可以直接在 HardFault_Handler 里打断点,然后查看MSP或PSP指针附近的栈数据,手工还原调用现场。这个能力在复杂的嵌入式项目里非常有用。

8. 从启动流程看工程最佳实践

启动流程虽然只是芯片运行的第一步,但它在整个嵌入式项目里决定了系统的稳定性基座。以下实践建议来自多个真实项目的经验总结。

8.1 始终保持向量表正确

使用 STM32CubeMX 生成工程时,向量表由工具自动生成,不需要手动维护。如果你的项目是从旧工程拷贝修改而来,要特别注意:

  • 不要在新芯片上使用旧工程的启动文件。
  • 不同容量、不同系列的启动文件不能混用。
  • 手动添加中断后,记得检查启动文件中的__Vectors定义是否包含对应入口。

8.2 bootloader 跳转 App 的启动要点

如果你的项目采用 bootloader + app 架构,跳转前必须确认:

  1. App 的链接起始地址是偏移后的地址(例如0x08008000)。
  2. App 的向量表偏移量已经设置。
  3. 跳转前关闭全局中断、关闭相关外设时钟。
  4. 跳转函数需要把 MSP 设置为 App 首地址处的栈顶值。

典型跳转代码:

typedef void (*pFunction)(void); void JumpToApp(uint32_t app_addr) { uint32_t JumpAddress = *(volatile uint32_t *)(app_addr + 4); pFunction Jump_To_App = (pFunction)JumpAddress; __disable_irq(); /* 搬移 MSP 为 App 的栈顶 */ __set_MSP(*(volatile uint32_t *)app_addr); Jump_To_App(); while (1) { } }

注意,跳转前最好将 RCC 时钟配置恢复到复位状态,避免 App 端初始化外设时出现意外。

8.3 时钟源选择的工程权衡

在启动阶段用 HSI 可以快速启动,不依赖外部晶振,但精度有限。对于需要 USB 通信或 CAN 通信的项目,最好使用 HSE + PLL。一般来说:

  • 低功耗产品:可先用 HSI,进入低功耗前关闭外部晶振。
  • USB 产品:必须使用精度足够高的时钟源。
  • 量产稳定性优先:外部晶振必须有合适的负载电容,启动代码里要对HAL_RCC_OscConfig的返回值做处理。

8.4 合理设置栈大小

在裸机开发中,启动文件里的Stack_Size就是系统主栈。它同时承担中断嵌套的现场保存和main函数的局部变量开销。推荐做法:

  • 中断回调里不定义大数组。
  • 局部大数组用static修饰,放到静态区。
  • 递归调用尽量改为循环。
  • 如果使用了 RTOS,则每个任务栈由内核创建,但中断栈仍使用启动文件中的主栈。

9. 对照 Flipper Zero 的嵌入式实战视角

Flipper Zero 是一个典型的开源嵌入式硬件项目,它基于 STM32WB55 系列芯片,内部跑着多层固件结构:Bootloader、核心固件、各种外设驱动和蓝牙协议栈。在这个项目中,启动流程的严谨性往往决定了整个系统的稳定性表现。

Flipper Zero 固件在启动阶段会做几件非常有代表性的工作:

  1. 根据硬件版本选择不同的外设初始化策略。
  2. 早期初始化电源管理和时钟。
  3. 判断是否需要进入 DFU(Device Firmware Upgrade)模式。
  4. 初始化 Flash 文件系统,加载配置。
  5. 最终进入主事件循环。

这种设计思路实际上就是把“启动流程”从“能跑就行”提升到了“平台化”的高度。即使你是从零开始学习 STM32,也建议把自己写的工程当做一个“平台”来管理:

  • 启动文件只做芯片级初始化,不做业务逻辑。
  • 把硬件版本判断、时钟选择、错误记录放到单独模块中。
  • 在SystemInit之后、进入main之前,预留必要的早期异常处理路径。
  • 使用WEAK函数给每一个中断服务函数提供默认实现,避免链接错误。

从这一个角度出发,你会发现自己对嵌入式系统的理解会上升一个层次。

10. 总结与后续学习建议

本文从 Cortex-M 的向量表机制讲起,依次分析了 STM32 启动文件中的栈空间、向量表、Reset_Handler,再到 C 库的__main如何准备 C 运行环境,最后用 CubeMX 生成了一个最小工程,通过仿真断点完整观察了启动流程。还汇总了嵌入式开发中最常见的启动类故障以及排查方法。

如果你目前正在学 STM32,下一步可以做几件事:

  1. 打开手头任意一个 STM32 工程,从启动文件的 Reset_Handler 开始单步调试,跑完整个启动过程。
  2. 自己手动修改一次链接地址和向量表偏移,做一个最小的 bootloader + app 实验。
  3. 试着在 SystemInit 之前点亮一颗 LED,体验“比系统时钟配置更早的代码执行”。
  4. 如果你对开源硬件和系统级固件感兴趣,可以对照 Flipper Zero 的官方固件仓库,研究它的启动目录和平台初始化代码,模仿其分层方式重构一遍自己的裸机工程。

整体来看,启动流程是嵌入式开发入门的分水岭。搞懂了这一块,后面的时钟树、中断系统、外设驱动、RTOS 移植都会顺畅很多。如果本文对你有帮助,建议先收藏,后面调试遇到启动类问题时,可以回来对照排查。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询