[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