最近剛完成自己8266的小項目,已經發布在github上,有興趣的朋友可以看一下
github地址:esp-ujn
1. 通過MQTT協議與服務器交互
2. 內置HTTP服務器,支持通過瀏覽器進行參數配置
編譯流程分析
我們在編譯8266代碼時可以使用項目中的gen_misc.sh(Windows下為gen_misc.bat)腳本,選擇合適的參數后就會在sdk/bin/文件夾中生成可燒錄的文件,如eagle.flash.bin,eagle.irom0text.bin。 但這樣存在的問題是每次編譯時都需要選擇一遍編譯參數,所以一般會使用make命令進行編譯,如:
make COMPILE=gcc BOOT=none APP=0 SPI_SPEED=40 SPI_MODE=QIO SPI_SIZE_MAP=4
這是因為gen_misc.sh的作用僅僅是供用戶選擇編譯參數,最終的編譯過程是通過make命令依據Makefile文件中定義的若干規則來進行的。接下來通過如下幾個方面來探討整個編譯流程
一、Makefile的組織形式
SDK中Makefile文件以樹形結構組織。總體上分為3類:主文件,項目配置文件,庫配置文件。
|--sdk/ |----Makefile |----project/ |------Makefile |------user/ |--------Makefile |------json/ |--------Makefile
如上圖所示
- sdk/Makefile 主文件
- sdk/project/Makefile 項目配置文件
- sdk/project/json/Makefile 庫配置文件
平常開發過程中,一般我們只需要關注項目配置文件與庫配置文件即可。如有時為了程序的模塊化,需要將不同的功能模塊編譯成獨立的庫。這時需要修改項目配置文件,並創建對應的庫配置文件。例如我們需要添加一個json庫。這時就需要:
- 在sdk/project/下創建文件夾sdk/project/json/
- 將sdl/project/user/Makefile拷貝到sdk/project/json/下
- 修改sdk/project/json/Makefile
- 修改sdk/project/Makefile
需要在兩個Makefile中做出的改動如下:
#sdk/project/json/Makefile GEN_LIBS = libjson.a #庫名 #sdk/project/Makefile SUBDIRS = user \ json #庫目錄 COMPONENTS_eagle.app.v6 = user/libuser.a \ json/libjson.a #庫路徑
二、燒錄文件的生成過程
對於Non-FOTA模式,編譯完成后會在sdk/bin/目錄下生成eagle.flash.bin與eagle.irom0text.bin。顯然這兩個文件並不是編譯器的直接產物,一般來說編譯器會通過我們的代碼生成一個可執行程序。那么這兩個文件是從何而來的呢?實際上這兩個文件是編譯后生成的可執行文件的一部分。可執行文件被拆解成了多個部分,然后拼湊出了這兩個文件供我們燒錄。我們的代碼經過編譯后會生成一個elf文件,它的路徑在sdk/project/.output/eagle/debug/image/eagle.app.v6.out。這個一個標准的elf文件,可以使用readelf命令查看它的一些信息。
#readelf -h ELF 頭: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 類別: ELF32 數據: 2 補碼,小端序 (little endian) 版本: 1 (current) OS/ABI: UNIX - System V ABI 版本: 0 類型: EXEC (可執行文件) 系統架構: Tensilica Xtensa Processor 版本: 0x1 入口點地址: 0x40100004 程序頭起點: 52 (bytes into file) Start of section headers: 549292 (bytes into file) 標志: 0x300 本頭的大小: 52 (字節) 程序頭大小: 32 (字節) Number of program headers: 5 節頭大小: 40 (字節) 節頭數量: 19 字符串表索引節頭: 16 #readelf -S 共有 19 個節頭,從偏移量 0x861ac 開始: 節頭: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .data PROGBITS 3ffe8000 0000e0 000804 00 WA 0 0 16 [ 2] .rodata PROGBITS 3ffe8810 0008f0 0015d0 00 A 0 0 16 [ 3] .bss NOBITS 3ffe9de0 001ec0 006f18 00 WA 0 0 16 [ 4] .irom0.text PROGBITS 40210000 0092c0 038b44 00 AX 0 0 16 [ 5] .text PROGBITS 40100000 001ec0 0073fc 00 AX 0 0 4 [ 6] .xtensa.info NOTE 00000000 041e04 000038 00 0 0 1 [ 7] .comment PROGBITS 00000000 041e3c 001bbd 00 0 0 1 [ 8] .debug_frame PROGBITS 00000000 0439fc 00211c 00 0 0 4 [ 9] .debug_info PROGBITS 00000000 045b18 014bec 00 0 0 1 [10] .debug_abbrev PROGBITS 00000000 05a704 003f8a 00 0 0 1
將可執行文件eagle.app.v6.out轉變為可燒錄文件的過程定義在sdk/Makefile,也就是在主文件中。大體流程如下:
#將.text、.data、.rodata和.irom0.text節的數據轉存到文件 objcopy --only-section .text -O binary eagle.app.v6.out eagle.app.v6.text.bin objcopy --only-section .data -O binary eagle.app.v6.out eagle.app.v6.data.bin objcopy --only-section .rodata -O binary eagle.app.v6.out eagle.app.v6.rodata.bin objcopy --only-section .irom0.text -O binary eagle.app.v6.out eagle.app.v6.irom0text.bin #通過eagle.app.v6.text.bin、eagle.app.v6.data.bin和eagle.app.v6.rodata.bin生成eagle.app.flash.bin python sdk/tools/gen_appbin.py eagle.app.v6.text.bin eagle.app.v6.data.bin eagle.app.v6.rodata.bin #將最后生成的可燒錄文件放到sdk/bin/目錄下 mv eagle.app.flash.bin sdk/bin/eagle.flash.bin mv eagle.app.v6.irom0text.bin sdk/bin/eagle.irom0text.bin
通過上邊readelf -S獲取到的節區表信息可以看到,實際在內存中出現的節只有.text、.data、.bss、.rodata和.irom0.text。
.text + .data + .rodata => eagle.flash.bin
.irom0.text => eagle.irom0text.bin
通過比較這幾個節的大小與燒錄文件大小的關系可以得到相同的結果(eagle.flash.bin文件中除了包含程序節數據,還有少量的配置數據)。.bss節雖然在內存中出現但是在程序初始化時會被整個清零,所以不必出現在燒錄文件中。這幾個節包含的數據內容如下:
節名 | 作用 |
.text | 存放代碼 |
.data | 存放已初始化的全局變量 |
.bss | 存放未初始化的全局變量 |
.rodata | 存放只讀數據 |
.irom0.text | 存放標注有ICACHE_FLASH_ATTR的代碼或ICACHE_RODATA_ATTR的變量 |
三、Makefile的執行過程
在前邊已經提到,我們寫的代碼最終會編譯為一個elf格式的可執行文件(eagle.app.v6.out),接下我們通過具體Makefile文件中的代碼對整個編譯的執行過程進行分析。之前講到sdk/Makefile為主文件,也就是所有編譯時用到的邏輯都在其中定義。我們整個的編譯流程中需要按順序產生如下幾類目標:二進制目標文件、庫文件、elf文件、燒錄文件。那么如何通過一個主Makefile來完成這些工作呢,這里需要先看一下其余兩類起配置作用的Makefile。這兩類Makefile的最后都會有如下代碼:
PDIR := ../$(PDIR) sinclude $(PDIR)Makefile
它的作用是包含自己父文件夾中的Makefile文件,那么最后主Makefile文件中的內容會被包含到庫配置文件與項目配置文件中。在項目配置文件中,它的作用是產生elf可執行文件與燒錄文件,在庫配置文件中,它的作用是產生靜態鏈接庫。主Makefile中,最主要的顯式規則如下,通過這兩條規則產生了所有我們需要的文件。
...
314 all: .subdirs $(OBJS) $(OLIBS) $(OIMAGES) $(OBINS) $(SPECIAL_MKTARGETS)
... 324 .subdirs: 325 @set -e; $(foreach d, $(SUBDIRS), $(MAKE) -C $(d);)
這兩條規則的目標都是偽目標,並不會產生任何文件,我們所需要的所有文件都是目標all的依賴文件。
$(OBJS) |
二進制目標文件 |
$(OLIBS) |
靜態鏈接庫 |
$(OIMAGES) |
elf可執行文件 |
$(OBINS) |
燒錄文件 |
$(SPECIAL_MKTARGETS) |
一直為空 |
第一個依賴文件.subdirs是一個偽目標,也就是每次編譯時都會先執行.subdirs中定義的操作,也就是遍歷所有含Makefile文件的子文件夾,執行make命令。通過這種方式產生的結果就是make工具的當前路徑發生了改變。拿上邊列出的項目結構為例,我們一次編譯過程可分為
- 我們在sdk/project/文件夾下執行make命令編譯源碼
- sdk/project/Makefile包含sdk/Makefile的內容
- 構建目標all
- 依賴文件.subdirs不存在,進行構建
- 遍歷sdk/project/下所有含Makefile文件的子文件並執行make命令
這時如果執行了sdk/project/json/Makefile,那么此時make工具的當前路徑變為了sdk/project/json/。此時sdk/project/json/Makefile對上層Makefile進行包含后再次構建目標all。一般來說sdk/project/json/中不會再有包含Makefile的子文件夾,那么此時目標all的第一個依賴項.subdirs會立刻返回,然后再對其余的依賴項進行構建。
還有一點需要說明的是目標all的依賴項並不是全都有值的,比如$(SPECIAL_MKTARGETS)的值就一直為空,表示不存在此依賴項。繼續拿上邊的項目結構舉例:
make當前路徑 | $(OBJS) |
$(OLIBS) |
$(OIMAGES) |
$(OBINS) |
sdk/project/ | 空 | 空 | eagle.app.v6.out | eagle.app.v6.bin |
sdk/project/json/ | json.o | libjson.a | 空 | 空 |
根據make當前路徑的不同,目標all有不同的依賴項,然后再根據主Makefile中定義隱式規則對依賴項進行構建,即完成了整個項目的構建過程。
有的朋友可能對Makefile的語法不熟悉,這里推薦一個網站
https://www.gnu.org/s/make/manual/make.html
官方的教程很詳細