1. 程式人生 > 其它 >[esp8266]RAM不足問題,導致重啟

[esp8266]RAM不足問題,導致重啟

RAM不足

問題描述

在esp8266程式設計過程中,它擁有DRAM80KB,但是使用時,想要申請一塊固定的全域性變數BUffer時,只能使用到8K
再往上就會照成重啟問題,明明只已經使用了50%。

問題分析

  1. 實際上RAM只能使用50%
  2. 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.binfirmware.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的結構講解。

.bss.data.text.段講解

readelf用法

一些命令

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