STM32F103C8T6
前言
记录学习STM32F103C8T6中遇到的某些概念的理解,问题的排查思路解决方法,总结等。内容上可能是个大杂烩。
概念理解
MCU 内存结构与变量存储详解
——以 STM32F103C8T6 为例
以 STM32F103C8T6(Cortex-M3,72MHz,20KB RAM,64KB Flash) 为例,系统性地梳理 MCU 的内存结构和变量存储规则。
ROM vs RAM:本质区别
| 特性 | ROM(Flash) | RAM(SRAM) |
|---|---|---|
| 全称 | Read-Only Memory(只读存储器) | Random Access Memory(随机存取存储器) |
| 物理特性 | 非易失性(断电不丢数据) | 易失性(断电清零) |
| 用途 | 存放程序代码 + 常量数据 | 存放运行时变量 + 栈 + 堆 |
| 读写速度 | 较慢(需 flash controller) | 极快(CPU 直接访问) |
| 可写次数 | 有限(约 10k~100k 次) | 无限 |
| 在 STM32 中 | 64KB Flash(地址 0x08000000 起) | 20KB SRAM(地址 0x20000000 起) |
💡 简单记:
- Flash = 硬盘 → 存程序、常量
- RAM = 内存条 → 存运行时数据
STM32 内存布局(启动后)
地址空间
┌───────────────────────┐ ← 0x08010000 (0x08000000 + 64KB)
│ Flash │ ← 程序代码、const 变量、字符串常量
│ (Code + Constants) │ 如: "Hello", const int x = 5;
└───────────────────────┘ ← 0x08000000
| |
┌───────────────────────┐ ← 0x08010000 (64KB)
│ .rodata │ ← 只读数据:const 变量、字符串字面量
│ (Read-Only Data) │ 如: const char logo[] = {0x01,0x02,...};
│ │ const int VERSION = 1;
│ │ "Hello", "Error: %d\n"
├───────────────────────┤
│ .text │ ← 程序代码(机器指令)
│ (Code / Text) │ 所有函数的编译后二进制
├───────────────────────┤
│ .init_array │ ← C++ 全局构造函数表(C 项目通常为空)
├───────────────────────┤
│ .ARM.exi / .ARM.ext │ ← 异常 unwind 表(用于异常回溯,C 项目可忽略)
├───────────────────────┤
│ Vector Table │ ← 中断向量表(前 0x100~0x200 字节)
│ (at 0x08000000) │ 包含初始栈顶(__initial_sp)、Reset_Handler 地址等
└───────────────────────┘ ← 0x08000000
(中间是外设寄存器等,略)
┌───────────────────────┐ ← 0x20005000 (0x20000000 + 20KB)
│ SRAM │
├───────────────────────┤
│ Stack │ ← 主栈(main 函数及中断使用),向下增长
├───────────────────────┤
│ Heap (可选) │ ← 动态分配(malloc),向上增长
├───────────────────────┤
│ Uninitialized Data │ ← `.bss` 段:未初始化的全局/静态变量(如 int x;)
├───────────────────────┤
│ Initialized Data │ ← `.data` 段:已初始化的全局/静态变量(如 int y = 10;)
└───────────────────────┘ ← 0x20000000
⚠️ 注意:Stack 和
.bss/.data之间没有隔离!栈溢出会覆盖全局变量。
变量到底存在哪里?
1. 全局变量(Global Variables)
int global_var = 100; // → .data 段(已初始化)
int uninit_global; // → .bss 段(未初始化,默认=0)
- 生命周期:整个程序运行期间
- 存储位置:RAM 的
.data或.bss段 - 特点:地址固定,可被所有函数访问
其中,
.data段变量的初始值在程序烧录时存储于 Flash 的只读区域,系统启动时被复制到 RAM;而.bss段变量不占用 Flash 空间,启动时由启动代码将其对应的 RAM 区域清零。
2. 局部变量(Local Variables)
void func() {
int local_var = 200; // → 栈(Stack)
char buf[100]; // → 栈(Stack)
}
- 生命周期:函数调用期间
- 存储位置:栈(Stack)
- 特点:
- 每次调用函数,就在栈上分配空间
- 函数返回时自动释放
- 栈空间有限!STM32 默认仅 1KB,可按需分配
3. 加了 static 的变量
(a) 静态全局变量
static int static_global = 300; // → .data 段
- 作用域限制在本文件(其他 .c 文件不可见)
- 存储位置:仍在 RAM 的
.data/.bss段,和普通全局变量一样
(b) 静态局部变量
void func() {
static int static_local = 400; // → .data 段(不是栈!)
}
- 生命周期:整个程序运行期间(只初始化一次)
- 存储位置:RAM 的
.data/.bss段(不是栈!) - 关键优势:不占用栈空间!
✅ 这就是为什么把函数中的 char buf[160] 改成 static 能缓解栈溢出!
4. 常量(Constants)
const int CONST_VAR = 500; // → Flash(.rodata 段)
const char* str = "Hello"; // 字符串 "Hello" 在 Flash,指针在 RAM
- 存储位置:Flash(节省 RAM!)
- 注意:不能修改(否则触发硬件 fault)
什么时候该用 static?
| 场景 | 是否推荐static | 原因 |
|---|---|---|
| 大缓冲区(>100 字节) | ✅ 强烈推荐 | 避免栈溢出 |
| 仅在本文件使用的函数/变量 | ✅ 推荐 | 隐藏实现,减少命名冲突 |
| 需要"记忆"上次值的局部变量 | ✅ 必须用 | 如计数器、状态机 |
小整数、指针等(<16 字节) | ❌ 不必 | 栈开销小,保持函数纯度更好 |
💡 嵌入式黄金法则: “大数组不用局部,优先 static 或全局”
如何查看变量到底在哪?
编译后查看 .map 文件(Keil 默认生成):
.data section:
global_var 0x20000000 Data 4
static_global 0x20000004 Data 4
.bss section:
uninit_global 0x20000008 Zero Init 4
static_local 0x2000000C Zero Init 4
Stack size: 0x00000400 (1024 bytes)
还能看到:
- 各段大小
- 符号地址
- 最大栈使用估算(如果开启)
总结:一张表看懂变量存储
| 变量类型 | 示例 | 存储位置 | 生命周期 | 是否占栈 |
|---|---|---|---|---|
| 全局变量 | int x = 1; | RAM (.data) | 整个程序 | ❌ |
| 未初始化全局 | int y; | RAM (.bss) | 整个程序 | ❌ |
| 局部变量 | int z = 2; | 栈 (Stack) | 函数内 | ✅ |
| 大局部数组 | char buf[200]; | 栈 (Stack) | 函数内 | ✅(危险!) |
| static 局部 | static int s = 3; | RAM (.data/.bss) | 整个程序 | ❌ |
| const 常量 | const int c = 4; | Flash | 整个程序 | ❌ |
| 字符串字面量 | "Hello" | Flash | 整个程序 | ❌ |
STM32 栈与堆分配
Keil 启动文件
🔹 什么是栈(Stack)和堆(Heap)?
- 栈:用于函数调用、局部变量,从 SRAM 高地址向下增长(如
0x20005000 →) - 堆:用于
malloc()/free()动态分配,从.bss后向上增长 - 全局/静态变量:存于
.data/.bss,位于 SRAM 低地址
✅ 正确内存布局(高地址在上):
0x20005000 ──┐
│ Stack(向下 ⬇️)
├───────────────
│ Heap(向上 ⬆️)
├───────────────
│ .bss / .data
0x20000000 ──┘
🔧 何时需要修改?
| 场景 | 栈(Stack_Size) | 堆(Heap_Size) |
|---|---|---|
| 简单控制(LED/按键) | ❌ 几乎不用改 | ❌ 不用 |
| 通信协议(MQTT/HTTP) | ✅ 必须改(大缓冲区) | ⚠️ 若用 malloc |
| 图形显示(OLED/LCD) | ✅ 常需改(局部缓存) | ❌ 通常不用 |
| RTOS / 文件系统 | ✅ 每任务独立栈 | ✅ 需要堆 |
💡 经验:栈常需调整,堆只在明确使用动态分配时才开。
🚨 如何判断是否需要改?
-
栈不足迹象:
- 程序死机、HardFault
- 全局变量被莫名修改(如 OLED 显示乱码)
- 使用大局部数组(
char buf[200])后异常
-
堆不足迹象:
malloc()返回NULL- 动态分配失败但逻辑无误
🔥 根本原因:栈溢出会覆盖
.bss中的全局变量,造成“幽灵 bug”
⚙️ 推荐配置(STM32F103C8T6,20KB RAM)
Stack_Size EQU 0x00001000 ; 4KB —— 安全默认值
Heap_Size EQU 0x00000200 ; 512B —— 谨慎预留(见说明
为什么 4KB?
足够应对深层函数调用、协议栈(如 MQTT/HTTP)、局部大数组等常见场景。
为什么保留 512B 堆?
- 即使未显式调用
malloc(),某些 C 库函数(如sprintfwith%f)或第三方库可能隐式使用堆 - 若确认使用 MicroLIB 且完全无动态分配,可设为
0x000 - 512B 仅占 2.5% RAM,却能避免隐蔽崩溃,推荐作为安全余量
✅ 激进点?
即使设为 8KB 栈(0x2000),只要总用量 < 20KB,依然安全!
⚠️ 注意事项
- 栈 + 堆 + 全局变量 ≤ 20KB
- 栈太小 → 溢出 → 覆盖全局变量(如 OLED 缓存乱码)
- 栈太大 → 挤占全局变量空间(但比溢出更安全)
- 不要盲目保留默认 512B 堆而不思考用途,也不要武断关闭堆而不验证依赖
🔍 实用技巧
- 查看
.map文件:确认各段实际地址与大小 - 口诀:
“全局在底,堆往上走,栈在顶头,往下开口。”
💡 核心原则:
在资源允许下,栈“宁大勿小”;堆“按需预留,不用可关”。
主动规划内存,是写出健壮嵌入式系统的第一步。
遇到的问题
栈溢出导致OLED显示乱码
🎯 问题现象
在开发基于 STM32F103C8T6 + SSD1306 OLED(128×64) 的物联网设备时,我设计了如下显示布局:
- 左侧(X=0~63):固定显示启动 Logo
- 右侧(X=66~127):动态显示初始化状态(DHT11、ESP8266、OneNET 连接进度)
预期行为:在调用 OLED_Clear() 前,Logo 应始终保持不变。
但实际运行中:
- ✅ DHT11、ESP8266 阶段:Logo 正常
- ❌ 执行到 OneNET MQTT 连接阶段:左侧 Logo 出现乱码
更诡异的是:
OneNet_DevLink()前 显示"Test0"→ Logo 正常OneNet_DevLink()后 显示"Test1"→ Logo 乱码- 若在
"Test1"前重绘 Logo(OLED_Clear()+OLED_ShowImageArea),则显示正常
→ Logo 数据被意外修改,但非 OLED 驱动逻辑错误。
🔍 排查过程与关键线索
第一步:排除驱动问题
- 确认
OLED_ClearArea(66, 0, 62, 63)不覆盖 X=0~63 - 检查
OLED_ShowString无越界写入 - 前期阶段正常 → 驱动基本可靠
第二步:定位触发点
通过测试字符串缩小范围:
OLED_ShowString(66, 16, "Test0", ...); // Logo 正常
OneNet_DevLink(); // ← 关键函数
OLED_ShowString(66, 16, "Test1", ...); // Logo 乱码
深入发现,问题出现在:
OneNET_Authorization(..., authorization_buf, sizeof(authorization_buf), ...);
第三步:变量存储类型实验(关键突破!)
将局部数组:
char authorization_buf[160];
改为:
static char authorization_buf[160];
结果:Logo 依然乱码,但乱码图案发生变化!
→ 这一现象强烈暗示:内存布局改变影响了数据破坏位置——典型的内存踩踏(Memory Corruption)。
💡 注:排查中曾遇 Flash 超限报错(L6220E: LR_IROM1 size exceeds limit),但此为代码体积问题,与本次 RAM 污染无关,仅是优化提醒。
🧠 根本原因:栈溢出覆盖全局变量
综合所有线索,真相浮出水面:
OneNET_Authorization及其调用链使用大量局部变量(160 字节缓冲区 + 协议栈临时数据),导致 栈空间耗尽,向下溢出并覆盖了位于 RAM 低地址的全局变量OLED_DisplayBuf。
内存布局分析(来自 .map 文件)
0x20002BF0 ──┐ ← 初始栈顶(__initial_sp)
│
│ Stack(2KB,向下增长 ⬇️)
0x200023F0 ──┤ ← 栈理论底部
│
│ (空隙仅 140 字节!)
0x200021F0 ──┤ ← Heap(512B,向上增长 ⬆️)
0x20002164 ──┤ ← .bss 段结束
│
│ OLED_DisplayBuf[1024](位于 .bss 末尾!)
0x20001D64 ──┘ ← .bss 起始附近
为什么发生覆盖?
void OneNet_DevLink() {
char authorization_buf[160]; // 占用栈
// ... 其他局部变量 + 深层函数调用
} // 总栈使用 > 2KB → 溢出!
→ 栈向下增长,覆盖 .bss 末尾的:
uint8_t OLED_DisplayBuf[8][128]; // 全局变量
→ Logo 数据被破坏 → 乱码。
而改为 static 后:
- 不占栈,缓解压力
- 但改变了
.bss布局 → 覆盖位置偏移 → 乱码图案变化
⚠️ 注意:函数返回后栈指针恢复,但被污染的全局变量无法自动修复!
📊 以为是“20KB RAM 不足”?
不然:
| 项目 | 大小 |
|---|---|
| 全局变量(.bss) | ~8.3 KB |
| 堆(Heap) | 512 B(可关闭) |
| 原栈(Stack) | 2 KB(配额不足!) |
| 总计已用 | ≈10.8 KB |
| 剩余 RAM | ≈9.2 KB |
| ✅ 系统远未达内存瓶颈,问题纯粹是栈配额不合理! | 这是分配方法! |
对 MQTT/HTTP/JSON 等协议栈项目,分配 8KB 栈完全合理:
全局变量: ~8.3 KB
堆(关闭): 0 KB
栈(8KB): 8 KB
──────────────────
总计: ~16.3 KB < 20 KB ✅
🔑 关键澄清:
Stack_Size = 0x2000(8KB)仅预留地址空间,实际栈按需增长,未使用部分不占资源。
✅ 解决方案与工程建议
方案 1:增大栈空间(推荐)
在 startup_stm32f103xb.s 中:
Stack_Size EQU 0x00002000 ; 8KB —— 安全余量充足
Heap_Size EQU 0x00000000 ; 关闭堆(裸机通常不用 malloc)
优势:
- 彻底杜绝栈溢出
- 支持复杂协议栈
- 未来扩展无忧
方案 2:大缓冲区改 static(临时验证)
// ❌ 危险:占用栈
char buf[160];
// ✅ 安全:放 .bss 段
static char buf[160];
优点:快速验证;注意:非线程安全(裸机通常无此问题)
长期规范
- 禁止函数内定义 >100 字节数组
- 避免递归调用
- 关键全局变量尽量放 RAM 低地址(通过链接脚本控制)
- 定期检查 .map 文件中的内存布局
💡 写在最后
这次调试让我深刻体会到:
“实现功能是最简单的一步,难的是后续的优化和调试。” 这个“Logo 乱码”问题,表面是显示异常,实则是系统资源管理缺失的警钟。
🔍 快速自查清单
- 是否有函数内定义大数组(>100 字节)?
- 是否使用了协议栈(MQTT/HTTP/JSON)?
- Logo 或关键数据是否在复杂操作后异常?
- 改为
static后 bug 行为是否改变?
→ 若多个答案为“是”,优先怀疑栈溢出!
关于Keil的使用
Keil内存映射配置
这里的配置
🔹 ROM(Flash,64KB)
- 物理范围:
0x08000000 ~ 0x08010000(共 64KB) - Keil ROM Size: 默认设为
0x10000(65536 字节) - ROM用途:存放代码(
.text)、常量(.rodata)、中断向量表 - 可改为
0xFC00,后续的空间给用户数据存储(如掉电保存参数),储存范围是0x0800FC00 ~ 0x08010000 - ✅ 建议:若使用 Flash 存储功能,则将Size改为
0xFC00;否则可改满为0x10000。
🔹 RAM(SRAM,20KB)
- 物理范围:
0x20000000 ~ 0x20005000(共 20KB) - Keil 可调小 Size(如 18KB)
→ 仅在需要预留 RAM 给 DMA、USB 缓冲区等特殊用途时才缩小 - 日常开发应保持
0x5000(20KB),以充分利用资源、避免意外溢出 - ✅ 建议:无特殊需求?不要调小 RAM!
🧠 核心原则
- 配置不是“能不能”,而是“为什么”
- Flash/RAM 的起止地址和大小,是硬件与软件协同设计的关键边界
- 预留空间 = 主动规划,不是浪费
Keil 中 Use MicroLIB
——嵌入式 Flash 优化的关键技巧
在 STM32 裸机开发中,勾选 Use MicroLIB 可能会将 Flash 占用从 60KB 直接降至 52KB!
🔥 一句话总结
当做 无 OS 的裸机开发,且 不使用文件 I/O、浮点
printf、多线程 时,务必启用 MicroLIB。它用极简 C 运行时库替代默认庞大标准库,大幅减小体积。
✅ MicroLIB vs 默认 C 库
| 特性 | 默认 ARM C Library | MicroLIB |
|---|---|---|
| 体积 | >10 KB | <2 KB |
| 功能 | 完整(fopen, %f, errno, 多线程安全) | 仅基础(memcpy, sprintf 整数版) |
| 重入性 | 可重入(支持 RTOS) | 不可重入(仅单线程安全) |
浮点 printf | 支持 %f(但占 5–8KB) | ❌ 不支持(输出 ???) |
| 文件 I/O | 支持 | ❌ 不支持 |
✅ 何时使用?
- ✅ 推荐:STM32 裸机项目(如传感器 + OLED + 串口通信)
- ❌ 禁止:
- 使用 FreeRTOS 且多任务调用
printf/malloc - 必须使用
printf("%f", val)
- 使用 FreeRTOS 且多任务调用
💡 替代浮点打印:
float v = 3.1415;
printf("%d.%02d", (int)v, (int)(v*100) % 100); // 输出 "3.14"
⚙️ 如何启用?
- Options for Target → Target
- 勾选 ✅ Use MicroLIB
- 确保代码中 无
%f、无fopen、无errno
启动文件通常已提供
__user_setup_stackheap(),无需额外实现。
📊 为何节省 8KB+?
默认库为“通用性”链接了大量未用功能:
- 浮点
printf:5–8 KB → MicroLIB 仅 0.5 KB malloc/exit/locale/文件 stubs:共 ~3 KB → 全移除
✅ 最佳实践
- 所有裸机项目 默认开启 MicroLIB
- 禁止使用
%f(建立代码规范) - 避免
malloc,优先静态缓冲区 - 定期检查
.map文件,确认无_sys_open、_printf_fp等 full lib 符号
杂记
- STM32启动顺序
STM32 上电后先执行芯片厂商提供的汇编启动代码(Reset_Handler),完成栈初始化、.data/.bss 段设置和时钟配置后,才跳转到 main() 函数,并非直接从 main 开始。