Tip
本文从最基础的概念讲起,逐步深入到 MCU 上电启动的每一个细节。涵盖 MCU、ARM 内核、Flash、RAM、寄存器等核心概念,以及 Cortex-M 的向量表、启动代码、内存映射、中断机制等底层原理。
一、基本概念
1.1 什么是 MCU?
MCU(Microcontroller Unit,微控制器),可以理解为一台”浓缩在一颗芯片上的小电脑”。一台电脑有什么?CPU、内存、硬盘、主板上的各种接口(USB、网卡等)。MCU 把这些全部塞进了一颗芯片里:
| 电脑的部件 | MCU 中的对应 |
|---|---|
| CPU(中央处理器) | ARM Cortex-M 内核(或其他内核) |
| 内存条(RAM) | 片内 SRAM(几十 KB ~ 几百 KB) |
| 硬盘 | 片内 Flash(几十 KB ~ 几 MB) |
| 主板上的外设接口 | UART、SPI、I2C、GPIO、ADC 等外设 |
所以 MCU 就是:CPU 内核 + 存储器 + 外设 → 组成一颗芯片。相比于电脑,MCU 通电就能跑,不需要外部内存条和硬盘。常见的 MCU:STM32(意法半导体)、GD32(兆易创新)、ESP32(乐鑫)、ATmega328P(Arduino 用的那颗)。

1.2 什么是 ARM?
ARM 是一家英国公司。它不生产芯片,只设计 CPU 内核的电路方案(IP 核),然后授权给其他芯片公司使用。关键区分三个概念:

ARM 的 MCU 内核分三个经典系列:
| 系列 | 定位 | 代表型号 | 地址总线 | 常见用途 |
|---|---|---|---|---|
| Cortex-M | 微控制器 | M0/M0+/M3/M4/M7/M23/M33/M55/M85 | 32 位 | 嵌入式设备、IoT、工业控制 |
| Cortex-R | 实时处理器 | R4/R5/R8/R52/R82 | 32 位(部分 40 位) | 汽车刹车、硬盘控制器(要求极低延迟) |
| Cortex-A | 应用处理器 | A7/A53/A57/A72/A76/A78/A710/A715 | 32 位 / 64 位 | 手机、平板(跑 Linux/Android) |
| Cortex-X | 旗舰超大核 | X1/X2/X3/X4 | 64 位 | 旗舰手机的高性能核心(骁龙/天玑) |
| Neoverse | 服务器/云端 | N1/N2/V1/V2 | 64 位 | AWS Graviton 等云端芯片 |
| SecurCore | 安全芯片 | SC000/SC300 | 32 位 | 银行卡、SIM 卡、eSIM、安全元件 |
我们日常说的”STM32”用的就是 Cortex-M 系列内核。比如 STM32F103 用的是 Cortex-M3,STM32F407 用的是 Cortex-M4F(带 FPU 浮点单元)。除了 ARM,嵌入式领域还有两种常见内核架构:
| 8051 | Cortex-M | RISC-V(MCU 级) | |
|---|---|---|---|
| 架构 | 8 位 CISC | 32 位 RISC | 32/64 位 RISC |
| 地址空间 | 64KB | 4GB(统一编址) | 取决于实现(通常 4GB) |
| 指令集 | 复杂、变长、不规整 | Thumb/Thumb-2、定长 16/32 位混合 | RV32I 基础 + 模块化扩展 |
| 中断系统 | 简单固定向量 | NVIC — 硬件嵌套向量中断,支持抢占优先级 | CLIC/PLIC — 不同实现不同方案 |
| 工作模式 | 单一模式 | Handler 模式 + Thread 模式(特权/非特权) | Machine/Supervisor/User 多级 |
| 生态 | 老牌,8 位时代霸主 | 最成熟,ST/NXP/TI/Renesas 均采用 | 开源新势力,增长迅速 |
| 授权方式 | 多厂商兼容(Intel 授权) | ARM 公司收取 IP 授权费 | 开源 ISA,免授权费 |
Tip
Cortex-M 的核心优势: NVIC 的硬件自动压栈/出栈让中断响应极快(仅 12 个时钟周期);Thumb-2 指令集在代码密度和执行效率间取得很好平衡;统一的 4GB 内存模型让跨芯片移植非常方便,切换不同厂商的 MCU 只需改外设地址映射即可。
1.3 存储器
Flash 是一种非易失性存储器,掉电后数据不会丢失,类似电脑里的 SSD 固态硬盘。它的特点是读得快、写很慢,写入前必须先擦除一整块。在 MCU 中,Flash 用来存放两样东西:你的程序代码(编译后的机器指令),以及只读的常量数据(比如字符串、const 变量)。
SRAM(Static RAM,静态随机存取存储器)是一种易失性存储器,掉电数据立即丢失,类似电脑的内存条。它的特点是读写都非常快,但容量通常比 Flash 小很多。在 MCU 中,SRAM 是程序运行时的”工作台”——全局变量、局部变量、函数调用的堆栈、malloc 分配的堆都在这里。
寄存器是 MCU 中最快的存储单元,直接集成在 CPU 内核或外设模块内部,离 CPU 最近,读写只需一个时钟周期。它分为 CPU 内部寄存器(如 PC、SP、R0~R15)和外设寄存器(如 GPIO 输出寄存器、UART 状态寄存器),前者靠指令直接访问,后者靠地址映射访问。
| Flash | SRAM | 寄存器 | |
|---|---|---|---|
| 作用 | 存程序代码 + 常量数据 | 存运行时变量、堆栈 | 运算暂存、控制外设状态 |
| 类比 | 硬盘 / SSD | 内存条 | CPU 的工作台 |
| 掉电后 | 不丢失(非易失) | 丢失(易失) | 丢失(易失) |
| 速度 | 读较快,写很慢 | 读写都快 | 最快,单周期访问 |
| 容量 | 较大(几百 KB~几 MB) | 中等(几十 KB~几百 KB) | 极小(几十个) |
| 位置 | 片内独立模块 | 片内独立模块 | CPU 内部 / 外设模块内 |
Note
为什么程序不能直接从 Flash 运行? 实际上 Cortex-M 支持直接从 Flash 执行代码(通过 I-Bus 取指令),但 Flash 的访问速度比 SRAM 慢很多。高性能场景下会把关键代码搬到 RAM 里跑。
二、MCU 的内存模型
2.1 冯诺依曼 vs 哈佛架构
在理解内存地图之前,先搞清楚一个影响 MCU 整体设计的选择:CPU 怎么访问存储?
| 冯诺依曼架构 | 哈佛架构 | |
|---|---|---|
| 总线 | 指令和数据共用一条总线 | 指令和数据各自有独立总线 |
| 并发 | 取指令和取数据不能同时进行 | 取指令和取数据可以同时进行 |
| 瓶颈 | ”冯诺依曼瓶颈” — 总线带宽限制性能 | 硬件更复杂,芯片面积略大 |
| 典型应用 | 早期 PC(x86)、简单嵌入式 | DSP、Cortex-M MCU |
用一张图来直观对比:

Cortex-M 实际是”改进哈佛架构”。回忆第一章的芯片框图——内核伸出了三条总线:
- I-Bus:取指令专用通道,挂 Flash,CPU 可以随时从 Flash 读下一条指令
- D-Bus:读数据专用通道,挂 SRAM,CPU 可以同时读写运行时的变量
- System Bus:访问外设的通道(GPIO、UART 等的寄存器),以及 DMA 在 Flash 和 RAM 之间搬数据时走这里
这就是为什么 Cortex-M 能直接从 Flash 执行代码——哈佛架构下 Flash 有独立的 I-Bus 直达内核,取指令不占用数据通道。也正是这种总线结构,决定了接下来要讲的地址空间布局:Flash、SRAM、外设各有自己的地址范围,因为它们在物理上挂在不同总线上。
2.2 4GB 地址空间怎么来的
这里的”4GB”不是指 MCU 里有 4GB 的存储容量(实际上 Flash 和 SRAM 加起来也就几百 KB 到几 MB),而是指 CPU 能”看到”的地址范围——CPU 有 32 根地址线,能编出 2³² 个地址,每个地址指向 1 个字节,所以总共能访问 4GB 的地址空间。这 4GB 里只有一小部分对应真实的 Flash 和 SRAM,其余留给外设寄存器和未来的扩展。
为什么是 4GB?因为 Cortex-M 的地址总线有 32 根线(A0~A31),每根线只有 0 或 1 两种状态,总共能表示 2³² = 4,294,967,296 个不同地址。Cortex-M 按字节寻址,每个地址对应 1 个字节,所以总地址空间为 4,294,967,296 字节 = 4,194,304 KB = 4,096 MB = 4 GB。这 4GB 是数学上限,多一个地址都编不出来,因此 Cortex-M 的地址空间固定为 4GB。在 MCU 的世界里,所有东西都有一个地址。CPU 看到的整个世界就是一堆地址:

2.3 外设寄存器与地址映射
什么叫”寄存器”? 寄存器就是特定地址上的一小块存储空间。比如:
GPIOA 的输出数据寄存器 → 地址 0x40020014
往这个地址写 0x0001 → PA0 引脚输出高电平
往这个地址写 0x0000 → PA0 引脚输出低电平在代码中就是:
// 操作寄存器就是操作地址
*(volatile uint32_t *)0x40020014 = 0x0001; // PA0 输出高通常我们会用宏定义或结构体来封装,STM32 HAL 库已经把这一切包装好了。
三、MCU 上电启动全过程

这是核心部分。想象你按下设备的电源按钮,电涌进 MCU 的那一刻:
步骤 0:硬件复位
上电 → 电源稳定 → 复位电路释放复位引脚 → CPU 开始工作。
复位后,Cortex-M 内核自动做两件事:
步骤 1:取初始 SP 和 PC(硬件自动完成)
Cortex-M 的设计非常巧妙——它不需要任何软件,硬件自己会做前两步。
CPU 自动去地址 0x00000000 读两次:
地址 0x00000000 → 读出 4 字节 → 写入 MSP(主堆栈指针)
地址 0x00000004 → 读出 4 字节 → 写入 PC(程序计数器),CPU 从这里开始执行这两个值就存在 Flash 的最开头,也就是大家说的 向量表(Vector Table):

Tip
内存映射的”魔法”: 你可能会问——Flash 在
0x08000000,但 CPU 从0x00000000读,怎么读得到?答案是 地址重映射(Memory Remap)。启动时,STM32 会把 Flash 的起始地址(0x08000000)映射到0x00000000。CPU 读0x00000000,实际读到的是 Flash 开头的向量表。
步骤 2:执行 Reset_Handler(第一个函数)
CPU 跳转到 Reset_Handler 的地址,开始执行 启动代码。这部分代码通常用汇编写成(startup_xxx.s),典型内容:
// startup_stm32f103xb.s 中的 Reset_Handler(伪代码简化)
void Reset_Handler(void) {
// 1. 把 .data 段从 Flash 复制到 RAM
// (已初始化的全局变量:int a = 5;)
memcpy(&_sdata, &_sidata, &_edata - &_sdata);
// 2. 把 .bss 段清零
// (未初始化的全局变量:int b; 初始值应该是 0)
memset(&_sbss, 0, &_ebss - &_sbss);
// 3. 调用 main() 函数
main();
// 4. main 返回后死循环(正常情况下不应该返回)
while (1);
}步骤 3:程序的”搬家” — 数据段初始化
这里有个关键问题:已初始化的全局变量怎么办?
int counter = 42; // 初始值 42 存在哪里?Flash 是非易失的但写起来麻烦,RAM 掉电就丢。解决方案:

启动代码中的 copy/memset 就是在做这个”搬家”工作。
步骤 4:进入 main() — 你的代码开始运行
Reset_Handler 最后一句调用 main()。此时:
- 全局变量有正确的初始值了
- 堆栈指针指向 RAM 栈顶
- 外设还是默认状态(通常时钟没开)
所以在 main() 开头你通常要:
int main(void) {
HAL_Init(); // 初始化 HAL 库
SystemClock_Config(); // 配置系统时钟(外部晶振 → PLL → 72MHz/168MHz...)
MX_GPIO_Init(); // 初始化 GPIO
MX_USART1_UART_Init(); // 初始化串口
// ... 其他外设初始化
// 你的应用代码
while (1) {
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
HAL_Delay(500);
}
}四、深入理解:异常与中断
SysTick 定时器中断 是 MCU 的”心跳”。每 1ms 触发一次中断,HAL_Delay() 就靠它。中断发生时:
1. CPU 暂停当前执行的代码
2. 硬件自动保存现场(R0-R3, R12, LR, PC, xPSR → 压栈)
3. CPU 从向量表中查对应中断的入口地址
4. 跳转到 ISR(中断服务函数)
5. 执行完 ISR 后,硬件自动恢复现场(出栈)
6. 继续执行之前被打断的代码这一切都是 Cortex-M 硬件自动处理 的,不需要像 Cortex-A 那样写汇编保存上下文。
五、启动模式选择(以 STM32 为例)
STM32 根据 BOOT0 和 BOOT1 引脚的电平决定从哪里启动:
| BOOT0 | BOOT1 | 启动来源 |
|---|---|---|
| 0 | X | 用户 Flash(0x08000000)— 正常运行 |
| 1 | 0 | 系统存储器(内嵌 Bootloader)— 串口下载 |
| 1 | 1 | SRAM(0x20000000)— 调试用 |
平时正常运行就把 BOOT0 接地。要通过串口烧程序就拉高 BOOT0。
六、总结:几个”灵魂拷问”
Question
Q:MCU 一上电怎么知道从哪里开始跑?
A:Cortex-M 硬件从0x00000000读堆栈指针,从0x00000004读第一条指令地址(Reset_Handler)。
Question
Q:全局变量 int a = 10; 的 10 存在哪里?
A:烧录时存在 Flash 的 .data 段初始化数据里。启动代码在 main() 之前把它复制到 RAM 中对应的 .data 段位置。
Question
Q:为什么 static 局部变量不会丢?
A:static 变量本质上和全局变量一样,存在 RAM 的 .data 或 .bss 段,不随函数返回而销毁。
Question
Q:栈溢出会怎样?
A:MCU 的栈通常只有几 KB。如果递归太深或局部数组太大,栈会向下(地址减小方向)溢出到堆或 .data 区域,导致数据被莫名篡改,或者触发 HardFault。
Question
Q:HardFault 是什么?
A:当 CPU 做了非法操作(访问不存在的地址、除以零、未对齐访问等),硬件跳转到向量表中的 HardFault_Handler。开发时通常在里面加个 while(1) 断点来调试。
Note
如果你有具体想深入了解的部分(比如怎么读 STM32 的参考手册来写启动代码、链接脚本怎么写、或者想动手看一个最小启动示例),可以继续深入学习。