void OLEDS_Refresh(void) { u8 i, n; //这里可能会导致栈溢出 换成静态变量static static uint8_t buf[129]; /* buf[0]=控制码 0x40, buf[1..128]=像素数据 */ for (i = 0; i < 8; i++) { /* 设置页地址和列地址 */ OLEDS_WR_Byte(0xB0 + i, OLEDS_CMD); /* 页地址 0~7 */ OLEDS_WR_Byte(0x00, OLEDS_CMD); /* 列地址低 4 位 */ OLEDS_WR_Byte(0x10, OLEDS_CMD); /* 列地址高 4 位 */ /* 组装整页数据 */ buf[0] = 0x40; /* 控制码:数据 */ for (n = 0; n < 128; n++) { buf[n + 1] = OLEDS_GRAM[n][i]; } /* 一次 I2C 事务发完 129 字节 */ OLEDS_SendBulk(buf, 129); } }其中!!!!很重要里面的
static uint8_t buf[129]; /* buf[0]=控制码 0x40, buf[1..128]=像素数据 */
这个数组一开始没加静态变量 导致栈溢出,内存被踩,导致数据被写到别的地方,出现野数据,导致程序运行不了
一、解释:
1.什么是RAM?
(1)RAM定义:
(Random Access Memory,随机存取存储器),是计算机和单片机用来临时存放正在运行的程序和数据的存储器。
(2)特点:
掉电丢失:一旦断电,RAM中的所有内容立刻消失。所以他只能存临时数据,不能做永久存储。
读写数据速度快:比Flash,硬盘快的多,CPU直接从RAM中读取指令,读写变量。
可随机访问:任意地址都能直接读写,不需要像磁带一样顺序找。
(3)与ROM/Flash的区别:
在MSPM0G3507中,Flash存放代码,RAM存放全局变量,堆栈,临时计算数据。
(4)RAM的重要性:
堆栈:函数调用,局部变量,中断现场都占用RAM。中断嵌套或者大数组(比如OLED_Refresh中的buf[129]和环形缓冲区中的buffer[128]。
堆:动态内存分配(malloc)用的空间,嵌入式比较少用。
优化:若RAM不够,变量会被挤占(内存被踩),程序就会飞。合理安排数组大小和全局/局部变量的适用很关键。
二、内存布局:
MSPM0 RAM 从地到高大致是
0x20000000 ┌──────────────┐
│ .data / .bss │ ← 全局变量:track_state, OLEDS_GRAM, servo_x...
│ (静态区) │
├──────────────┤
│ 堆 (heap) │ ← malloc 用
│ ↓ │
│ │
│ ↑ │
│ 栈 (stack) │ ← 局部变量、函数调用返回地址
0x2000xxxx └──────────────┘
栈从RAM顶部向下长,全局变量从底部向上放,所以如果栈用太多,就会踩进全局变量区
三、在上面例子中加了static与不加static修饰数组对比
1.不加static修饰时:
void OLEDS_Refresh(void) { uint8_t buf[129]; // ← 129 字节全压在栈上 ... }129字节全压在栈上
在该项目里:
main() ~32 字节
→ Global_KeyDispatch() ~16 字节
→ OLEDS_ShowString() × 4次 ~64 字节 (累计 framebuffer 写入)
→ OLEDS_Refresh() ~20 字节 (局部变量 i, n)
+ buf[129] ~129 字节 ← 致命一击
→ OLEDS_SendBulk() ~16 字节
→ I2C_Start/SendByte/WaitAck ~32 字节
─────────────────────────────────────────
总计 ~309 字节
当309自己的栈怼到track_state所在的位置时,OLEDS_Refresh往buf[0..128]写数据,实际写到了track_state 的内存地址上,所以调试时候出现的状态(0,1,2)却出现值4就是这样来的,因为可能buf某个位置的像素数据恰好是0x04
2.加了static修饰时候:
void OLEDS_Refresh(void) { static uint8_t buf[129]; // ← 搬到 .bss 静态区,不占栈 ... }用static修饰成静态变量,搬到bss静态区,不占栈
栈用量:309 - 129 = 180字节,离track_state还远,踩不到它的内存
总结:static变量只初始化一次,存在静态区,和全局变量放在一起,它的地址是固定的,不会和栈争空间
四、为什么刚好踩到track_state的内存
track_state在Global.c里面定义。链接器按.o文件顺序排列BSS段,巧合的是track_state 离栈最近,栈溢出的第一个受害者就是它
五、更多发生内存被踩的原因
1.常见原因:
(1)数组越界:写入数组时,索引超出定义范围,覆盖了紧邻内存中的其他变量。
(2)栈溢出:局部变量太多或递归太深,导致栈空间用尽,会覆盖全局变量或堆上的数据。
(3)指针错误:野指针、未初始化指针、释放后还使用的指针,写入了非预期的地址
(4)中断与主循环竞争:一个变量在中断和主循环中同时被修改,但没有用volatile修饰或缺乏原子操作保护,导致数据不一致,看起来像被踩。
(5)编译器优化或内存对齐问题:结构体填充,位域操作不当,可能导致相邻字段被修改。
(6)DMA冲突:当DMA还在传输,CPU就去读写了同一片内存,数据可能损坏。
2.内存被踩典型表现:
(1)程序运行一段时间后,突然进入 Hard Fault 或其他异常。
(2)某个全局变量的值莫名其妙地改变,导致控制错乱。
(3)死机、重启、数据错乱,但断点调试时又似乎正常。
3.如何排查和解决?
(1)静态分析:仔细检查数组大小,循环边界,memcpy的长度。
(2)可以在关键变量防止特殊数字,定期检查是否被修改
uint32_t guard = 0xDEADBEEF; int my_data; uint32_t guard2 = 0xCAFEBABE; // 定时检查 guard 和 guard2 是否完好(3)增加MPU\堆栈保护:MSPM0有存储器保护单元,可以设置区域为只读来捕获非法写入。
(4)查看MAP文件:了解变量在内存中的布局,判断谁可能会踩到谁。
(5)在可疑代码段加断电数据观察点:调试器可以设置在某个内存地址被写入时自动停止。
(6)确保终端和主循环共享变量加volatile,必要时在写操作时关中断。
(7)保证DMA缓冲区不在CPU同时读写的时候使用,或者使用DMA_Cmd暂停,双重缓冲。