跳到主要内容

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 库函数(如 sprintf with %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"重绘 LogoOLED_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 LibraryMicroLIB
体积>10 KB<2 KB
功能完整(fopen, %f, errno, 多线程安全)仅基础(memcpy, sprintf 整数版)
重入性可重入(支持 RTOS)不可重入(仅单线程安全)
浮点 printf支持 %f(但占 5–8KB)❌ 不支持(输出 ???
文件 I/O支持❌ 不支持

✅ 何时使用?

  • ✅ 推荐:STM32 裸机项目(如传感器 + OLED + 串口通信)
  • ❌ 禁止
    • 使用 FreeRTOS 且多任务调用 printf/malloc
    • 必须使用 printf("%f", val)

💡 替代浮点打印:

float v = 3.1415;
printf("%d.%02d", (int)v, (int)(v*100) % 100); // 输出 "3.14"

⚙️ 如何启用?

  1. Options for Target → Target
  2. 勾选 ✅ Use MicroLIB
  3. 确保代码中 %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 符号

杂记

  1. STM32启动顺序

STM32 上电后先执行芯片厂商提供的汇编启动代码(Reset_Handler),完成栈初始化、.data/.bss 段设置和时钟配置后,才跳转到 main() 函数,并非直接从 main 开始。

加载评论中...