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/M8532 位嵌入式设备、IoT、工业控制
Cortex-R实时处理器R4/R5/R8/R52/R8232 位(部分 40 位)汽车刹车、硬盘控制器(要求极低延迟)
Cortex-A应用处理器A7/A53/A57/A72/A76/A78/A710/A71532 位 / 64 位手机、平板(跑 Linux/Android)
Cortex-X旗舰超大核X1/X2/X3/X464 位旗舰手机的高性能核心(骁龙/天玑)
Neoverse服务器/云端N1/N2/V1/V264 位AWS Graviton 等云端芯片
SecurCore安全芯片SC000/SC30032 位银行卡、SIM 卡、eSIM、安全元件

我们日常说的”STM32”用的就是 Cortex-M 系列内核。比如 STM32F103 用的是 Cortex-M3,STM32F407 用的是 Cortex-M4F(带 FPU 浮点单元)。除了 ARM,嵌入式领域还有两种常见内核架构:

8051Cortex-MRISC-V(MCU 级)
架构8 位 CISC32 位 RISC32/64 位 RISC
地址空间64KB4GB(统一编址)取决于实现(通常 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 状态寄存器),前者靠指令直接访问,后者靠地址映射访问。

FlashSRAM寄存器
作用存程序代码 + 常量数据存运行时变量、堆栈运算暂存、控制外设状态
类比硬盘 / 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 引脚的电平决定从哪里启动:

BOOT0BOOT1启动来源
0X用户 Flash(0x08000000)— 正常运行
10系统存储器(内嵌 Bootloader)— 串口下载
11SRAM(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 的参考手册来写启动代码、链接脚本怎么写、或者想动手看一个最小启动示例),可以继续深入学习。