前言:两个“内存”概念的冲突
初学 STM32 时,我们一定会遇到两个矛盾的说法:
- 数据手册说:Flash 64KB,RAM 20KB。
- 教程里讲:32 位单片机可以寻址 4GB 空间。
到底哪个是对的? ——两个都对,但说的是不同维度。
- 64KB Flash / 20KB RAM是芯片内部实际存在的物理存储器大小。
- 4GB 寻址空间是 CPU 能访问的地址范围(地址总线的能力),并不代表真有 4GB 的存储器。
就像你家有一个 100 平米的房子(实际存储),但你手里的钥匙可以打开整栋楼 1000 户的门(寻址能力)——只不过其中 990 户是空的(保留地址),你不能随便闯进去。
一、存储器映射:一张 4GB 的“地址地图”
存储器映射(Memory Map)是芯片设计者预先定义的一张表,规定了 4GB 地址空间里每一块区域用来干什么。
ARM Cortex-M 内核规定了一个统一的 4GB 地址布局,而芯片厂商(如 ST)在这个框架内填充自己的具体内容。
以 STM32F103 为例(地址从低到高):
| 起始地址 | 结束地址 | 大小 | 用途 | 说明 |
|---|---|---|---|---|
| 0x00000000 | 0x1FFFFFFF | 512MB | 代码区(Code) | 通常映射到 Flash,存放程序 |
| 0x20000000 | 0x3FFFFFFF | 512MB | SRAM 区 | 实际只有 20KB 在这里 |
| 0x40000000 | 0x5FFFFFFF | 512MB | 外设寄存器区 | GPIO、USART、定时器等寄存器 |
| 0x60000000 | 0xDFFFFFFF | 2GB | 外部存储器区 | FSMC 连接外部 SRAM/NOR |
| 0xE0000000 | 0xFFFFFFFF | 512MB | 内核私有区 | NVIC、SysTick、调试等 |
注意:大部分区域标注的是“保留”(Reserved),访问它们会导致 HardFault 或读出随机数据。
关键点:统一编址—— 无论是 Flash、RAM 还是外设寄存器,都被分配了独一无二的地址。CPU 不需要特殊的 I/O 指令,直接用ldr/str就能操作外设。这正是 C 语言指针可以操控寄存器的根本原因。
二、实际物理存储器在哪里?
虽然地图有 4GB 大,但芯片里真正焊接的“仓库”只有这几处:
- Flash:从
0x08000000开始(通过映射,也可从0x00000000访问)。大小通常 16KB~2MB。 - SRAM:从
0x20000000开始。大小通常 6KB~512KB。 - 外设寄存器:从
0x40000000开始,每个寄存器占 1~4 字节。
访问其他地址(比如0x30000000)会导致总线错误(HardFault)。所以,4GB 是理论寻址范围,不是物理存储容量。
三、什么是大小端模式?
大小端(Endianness)是指多字节数据在内存中的存储顺序。
假设有一个 32 位整数0x12345678,它在内存中的存放方式有两种:
大端模式(Big-Endian)
- 高字节存低地址,低字节存高地址。
- 地址 0x1000:
0x12,0x1001:0x34,0x1002:0x56,0x1003:0x78。 - 符合我们的阅读顺序(如:1234:1-千位,2-百位,3-十位,4-个位,从左到右读,但是左边代表的是最大的数)。
小端模式(Little-Endian)
- 低字节存低地址,高字节存高地址。
- 地址 0x1000:
0x78,0x1001:0x56,0x1002:0x34,0x1003:0x12。 - 这是 x86、ARM(默认)采用的方式。
为什么会有大小端?
主要是历史原因和 CPU 设计哲学差异。网络协议规定使用大端(称为网络字节序),而 PC 和大多数嵌入式处理器使用小端。因此跨平台通信时必须转换字节序。
四、如何判断你的系统是大端还是小端?
下面这段经典代码,利用了联合体(union)共享内存的特性:
#include<stdio.h>intmain(){union{uint32_tword;uint8_tbytes[4];}test;test.word=0x12345678;if(test.bytes[0]==0x78)printf("Little-endian\n");elseprintf("Big-endian\n");return0;}- 如果
bytes[0]是0x78,说明低地址存低位 → 小端。 - 如果
bytes[0]是0x12,说明低地址存高位 → 大端。
STM32(ARM Cortex-M)默认是小端模式,但有些 ARM 核心可以配置为大端(需要硬件支持)。
五、大小端对嵌入式开发的实际影响
1. 数据解析(串口、I2C、SPI 接收)
假设你通过串口收到两个字节0x12和0x34,协议规定这是大端的一个 16 位整数。
正确拼装:value = (buf[0] << 8) | buf[1];
如果协议是小端:value = (buf[1] << 8) | buf[0];
搞反了就会得到错误数值。
2. 寄存器访问
当你用 32 位指针访问一个 8 位寄存器数组时,大小端影响哪个字节对应哪个地址。不过外设寄存器通常按小端设计,直接赋值即可。
3. 网络通信
TCP/IP 协议规定使用大端。所以发送多字节整数前要用htonl/htons转换,接收后用ntohl/ntohs还原。
4. 文件存储
如果直接保存内存中的结构体到 SD 卡,换个不同端序的设备读取就会乱码。解决办法:统一使用小端或大端存储,或使用文本格式(如 JSON)。
六、一个容易混淆的概念:地址 vs 数据
很多新手会问:“地址 0x20000000 里存的是大端还是小端?”
大小端是针对多字节数据的存储顺序,地址本身没有端序。地址0x20000000是一个数值,它在指令中是以字节序列存在的,但 CPU 会自动处理。
七、STM32 的实际内存布局(以 F103C8T6 为例)
- Flash:
0x08000000~0x0800FFFF(64KB) - SRAM:
0x20000000~0x20004FFF(20KB) - 外设:
- GPIOA:
0x40010800~0x40010BFF - USART1:
0x40013800~0x40013BFF - TIM2:
0x40000000~0x400003FF
- GPIOA:
我们可以在参考手册的“Memory Map”章节找到完整列表。
八、常见误区与注意事项
“STM32 有 4GB 内存”❌
没有,只有几十 KB 到几 MB 的物理 RAM,但可以外扩。“指针可以随便指向任何地址”⚠️
指向保留地址会触发 HardFault。除非你确定该地址有有效设备。“大小端只在跨平台时重要”⚠️
即使本地开发,如果你通过 DMA 将数据从外设搬到 RAM,也要了解外设的端序(大多数与 CPU 一致)。“联合体判断大小端不通用”⚠️
在某些 DSP 或特殊架构上可能因对齐问题失效,但嵌入式 Cortex-M 上没问题。
九、总结
- 存储器映射:4GB 地址空间的分配表,包括代码、SRAM、外设、保留区。
- 实际物理存储:Flash 和 SRAM 只占地图的一小部分,其余地址访问会出错。
- 大小端:多字节数据的存储顺序(大端:高字节在低地址;小端:低字节在低地址)。
- STM32 默认为小端,但网络协议使用大端,通信时需要转换。
- 影响场景:数据解析、寄存器访问、网络通信、文件存储。
理解了存储映射,我们就知道*(uint32_t *)0x40010800 = 0x55;为什么能点亮 LED(因为那是 GPIO 寄存器的地址)。掌握了大小端,你就能轻松应对各种协议解析,不再被乱码困扰。
系列导航:
- 第一篇:嵌入式到底是什么?(含 ARM/C51/STM32 关系)
- 第二篇:串口江湖 —— UART、RS-232、RS-485
- 番外篇:波特率解析
- 第三篇:两线走天下 —— I2C 总线精讲
- 第四篇:极速先锋 —— SPI 总线精讲
- 第五篇:嵌入式大脑 —— 中断与事件驱动
- 第六篇:时间管理大师 —— 定时器与系统滴答
- 第七篇:存储与地址 —— 大小端、内存映射、4GB 空间之谜(本文)
- 第八篇:从裸机到 RTOS —— 任务、调度、FreeRTOS(预告)