[esp8266]RAM不足问题,导致重启
RAM不足
问题描述
在esp8266编程过程中,它拥有DRAM80KB,但是使用时,想要申请一块固定的全局变量BUffer时,只能使用到8K
再往上就会照成重启问题,明明只已经使用了50%。
问题分析
- 实际上RAM只能使用50%
- String类属于不定长,在函数中使用String,当全局已经使用90%情况下,无法为String申请更多空间。
解决过程
flash与RAM
实际上参与工作的硬件有w25q32和esp8266内部的ram区域。而flash其中一部分作为ROM,也就是存放代码。ram又被分为两部分:IRAM与DRAM。
那么也就是说代码应该被烧录到flash中。而代码被分为许多个section,常见的如:
- .bss
- .text
- .rodata
- .data
- .irom0.text
那么esp8266究竟的falsh与ram在代码运行究竟时怎样工作的?
探究工作过程
我这里使用的是一个esp8266 arduino库,开发工具为vscode+platformio来开发。芯片为esp-12s。
-
falsh大小:4M.
-
RAM:96K
我记得在操作系统的段页系统一节中,有cache一概念,它是介于cpu与RAM之间的,在指令流水出现结构冒险 时,通常解决办法是使用两个cache:icache与dcache。意思是指令cahce与数据cache。这里的IRAM与DRAM应该也是为了防止结构冒险吧。那么可以得出,IRAM是用来存放指令的,而DRAM用来存放数据的。
问题又来了IRAM存放怎么样的指令?在一篇文章ESP32 程序的内存模型可以得到:
IRAM实际上是执行代码,即.text段
从上面大致可以推出IRAM会将flash(w25q32)代码载入,并且执行。而IRAM又分为iram与icache,iram是真正的载入.text段的区域。那么icache的作用是?
官方给出的解释是:
后 32 KB 被映射作为 iCache,放在 SPI Flash 中的,加了ICACHE_FLASH_ATTR的代码会被从 SPI Flash 自动动态加载到 iCache。
这一段并没有讲明白映射大小,映射的位置,什么时候会自动加载到cache中。且代码相对较`大,不可能完全载入到32K iram中。
推测是32K iram会载入一部分指令给cpu使用,而icache则不断的预测代码位置,提前调入代码,就像虚拟内存的交换一样,在iram中未找到的指令,会去icache中寻找,当icahe也没有,则是产生缺失中断,将falsh中的代码调入icache,这部分是自动完成的。
该网页内容证实推断大致正确:https://blog.csdn.net/weixin_30657541/article/details/102172971
在前面一章flash的分布已经讲过icache映射在falsh的位置。这里再回忆一遍(文件eagle.flash.4m3m.ld
):
/* Flash Split for 4M chips */
/* sketch @0x40200000 (~1019KB) (1044464B) */
/* empty @0x402FEFF0 (~4KB) (4112B) */
/* spiffs @0x40300000 (~3048KB) (3121152B) */
/* eeprom @0x405FB000 (4KB) */
/* rfcal @0x405FC000 (4KB) */
/* wifi @0x405FD000 (12KB) */
MEMORY
{
dport0_0_seg : org = 0x3FF00000, len = 0x10
dram0_0_seg : org = 0x3FFE8000, len = 0x14000
iram1_0_seg : org = 0x40100000, len = 0x8000
irom0_0_seg : org = 0x40201010, len = 0xfeff0
}
PROVIDE ( _FS_start = 0x40300000 );
PROVIDE ( _FS_end = 0x405FA000 );
PROVIDE ( _FS_page = 0x100 );
PROVIDE ( _FS_block = 0x2000 );
PROVIDE ( _EEPROM_start = 0x405fb000 );
/* The following symbols are DEPRECATED and will be REMOVED in a future release */
PROVIDE ( _SPIFFS_start = 0x40300000 );
PROVIDE ( _SPIFFS_end = 0x405FA000 );
PROVIDE ( _SPIFFS_page = 0x100 );
PROVIDE ( _SPIFFS_block = 0x2000 );
INCLUDE "local.eagle.app.v6.common.ld"
这里的0x402xxxxx是esp8266定义的falsh map地址,而非硬件地址。
真实硬件地址应该是irom0_0_seg-0x40200000,这个是在flash(w25q32)的地址。
irom0_0_seg自然是rom的代码位置,它在esp8266 arduino flash分布图中应该是:
|--------------|-------|---------------|--|--|--|--|--|
^ ^ ^ ^ ^
Sketch OTA update File system EEPROM WiFi config (SDK)
在sketch位置。在arduino上sketch实际上是本意就是代码的意思。
现在有这样一个问题:
arduino中定义sketch是从0x40200000-0x402FEFEF,而irom0_0_seg是从0x40201010-0x40300000。明明sketch是代码的意思为什么与irom0_0_seg会有偏差(0x1010B)?
IRAM原理结论(大写IRAM与iram区别在于:IRAM包含iram)
实际上flash中的0x1010-0x100000
位置是irom0位置,ubuntu下使用readelf -S ./test/firmware.elf
(firmware.elf是编译后的烧录文件)查看确实irom0是在40201010
也没找到小于该地址的数据。
接着我想,那么从0x40200000-0x40201010
或者flash物理地址0-0x1010
究竟有什么,使用16进制查看器,发现确实在firmware.bin
0-0x1010
存在数据。那么该部分数据是什么?
想要去使用objdump,发现无法识别,那么为什么*.bin
文件无法识别,我向看看elf是如何转换成bin文件的,在Vscode编译,发现无法看到编译命令,从网上了解到objcopy可以将代码转换为bin。尝试arm-none-eabi-objcopy.exe -O binary .pio\build\esp12e\firmware.elf ./boot.bin
还是无法识别文件。那找找该工程的编译器,在.vscode/launch.json
中发现:
"toolchainBinDir": "C:/Users/PX_Lenovo/.platformio/packages/toolchain-xtensa/bin"
好家伙,编译器不是gcc,看了下xtensa
这个是esp系列处理器的型号,那这个就简单了,将上面指示的工具链位置加入系统路径,重启vscode,在vs(实际是powershell执行的)的命令行中执行:xtensa-lx106-elf-objcopy.exe -O binary .pio\build\esp12e\firmware.elf ./app.bin
。结果发现app.bin
与firmware.bin
不一样。
试过很多办法,都没办法找到任何相关信息,可能其产生是因为编译器自动产生的,作为一个文件的头部或者索引表
。
现在想要知道该文件怎么过来的,先得知道是怎么样得命令产生了firmware.bin
文件,想着加多调试信息,所以在platformio.ini
加入:
build_flags =
.....
-DCORE_DEBUG_LEVEL=5
编译一次,发现打印了如下关键信息:
Creating BIN file ".pio\build\esp12e\firmware.bin" using "C:\Users\PX_Lenovo\.platformio\packages\framework-arduinoespressif8266\bootloaders\eboot\eboot.elf" and ".pio\build\esp12e\firmware.elf"
里面指出在firmware.bin=eboot.elf+firmware.elf
所产生得。
irom0在代码中的表现是:irom0_0_seg,.irom0.text。而它所处的falsh位置是0x0001010-0x300000这一段位置。在cpu内存映射是0x40201010-0x40300000。32K的iram(iram1_0_seg)会调入部分irom0的代码进入其中,而有32K的icache(irom0_0_seg)作为虚拟内存映射到irom0上,增加读取速度。
DRAM结论
除去.text与.irom0.text就只剩下:.bss,.rodata,data三个字段。根据官方:
DRAM 空间为 96 KB: 对于 Non-OS_SDK,前 80 KB 用来存放 .data/.bss/.rodata/heap,heap 区的大小取决于 .data/.bss/.rodata 的大小;还有 16 KB 给 ROM code 使用。 对于 RTOS_SDK,96 KB 用来存放 .data/.bss/.rodata/heap,heap 区的大小取决于 .data/.bss/.rodata 的大小。
也就是说内存剩余空间就是heap得了,这里不知道有没有stack。heap是动态大小,这样就会带来一些问题:某些空间比较申请全局变量,那么就是.data字段了或者.bss字段。这个会压缩heap大小,且编译器无法识别如下错误:
以80K为例,当全局申请了20K得变量,但是某个函数中申请了70K得buffer,这样会使得跑入该函数时导致数据错乱。
缓解RAM不足办法
总之:尽量不要申请大得全局变量。
其他
官方问题合集里面有RAM的结构讲解。
一些命令
arm-none-eabi-nm -n -t d -S --size-sort .pio\build\esp12e\firmware.elf
arm-none-eabi-objdump.exe -t .pio\build\esp12e\firmware.elf > objdump.txt
arm-none-eabi-objcopy.exe -O binary ./firmware.elf boot.bin
arm-none-eabi-objcopy.exe -O binary .pio\build\esp12e\firmware.elf ./boot.bin
xtensa-lx106-elf-objcopy.exe -O binary .pio\build\esp12e\firmware.elf ./app.bin
xtensa-lx106-elf-objdump.exe -t .pio\build\esp12e\firmware.elf > objdump.txt
xtensa-lx106-elf-objdump.exe -t .pio\build\esp12e\firmware.bin > bin.txt
xtensa-lx106-elf-objdump.exe -h -b binary -m arm boot.bin
xtensa-lx106-elf-readelf.exe -h .pio\build\esp12e\firmware.elf > elfinfo.txt