ESP8266 Bootloader開源代碼解析之rboot(二)

ESP8266 Bootloader開源代碼解析之rboot(一)segmentfault

回顧

上一篇說了rboot的加載流程,主要的是經過makefile將兩個程序文件串了起來。這篇文章會對整個加載流程作詳細講解。數據結構

數據結構

typedef struct {
    /* magic是經常使用的名稱,用來標識這是個結構體,一般存在flash上,而且已經被初始化了 */
    uint8_t magic;           ///< Our magic, identifies rBoot configuration - should be BOOT_CONFIG_MAGIC
    /* 用來講明當前的數據結構適用於哪一個版本,不一樣版本常須要考慮兼容性問題 */
    uint8_t version;         ///< Version of configuration structure - should be BOOT_CONFIG_VERSION
    /* 當前rboot的啓動模式 */
    uint8_t mode;            ///< Boot loader mode (MODE_STANDARD | MODE_GPIO_ROM | MODE_GPIO_SKIP)
    /* 當前選擇的ROM區,但表示的是接下來要啓動的ROM區 */
    uint8_t current_rom;     ///< Currently selected ROM (will be used for next standard boot)
    /* MODE_GPIO_ROM模式下選擇的ROM區 */
    uint8_t gpio_rom;        ///< ROM to use for GPIO boot (hardware switch) with mode set to MODE_GPIO_ROM
    /* 可用的ROM總數 */
    uint8_t count;           ///< Quantity of ROMs available to boot
    /* 佔位,使前面長度夠32位整數 */
    uint8_t unused[2];       ///< Padding (not used)
    /* 每一個ROM區的地址 */
    uint32_t roms[MAX_ROMS]; ///< Flash addresses of each ROM
#ifdef BOOT_CONFIG_CHKSUM
    /* 本結構體的校驗值 */
    uint8_t chksum;          ///< Checksum of this configuration structure (if BOOT_CONFIG_CHKSUM defined)
#endif
} rboot_config;

其實自身的註釋就很詳細了。
一般作Bootloader的話,都會維護本身的一個數據結構,這個結構體中放着boot配置和信息,而且存放在flash中(掉電保存)。這樣作有幾個目的:less

  1. 知道使用哪一個ROM區進行啓動
  2. 能夠配置ROM區地址(更靈活)
  3. 保存版本信息,使更易兼容
  4. 能夠知道當前數據是否正確,有無損壞

檢查固件

若是把uint32_t find_image(void)函數簡化,是這樣的:ide

uint32_t NOINLINE find_image(void) {

    uint8_t flag;
    uint32_t loadAddr;
    uint32_t flashsize;
    int32_t romToBoot;
    uint8_t updateConfig = 0;
    uint8_t buffer[SECTOR_SIZE];

    rboot_config *romconf = (rboot_config*)buffer;
    rom_header *header = (rom_header*)buffer;

    ets_printf("\r\nrBoot v1.4.2 - richardaburton@gmail.com\r\n");

    // read boot config
    SPIRead(BOOT_CONFIG_SECTOR * SECTOR_SIZE, buffer, SECTOR_SIZE);
    // fresh install or old version?
    if (romconf->magic != BOOT_CONFIG_MAGIC || romconf->version != BOOT_CONFIG_VERSION
        ) {
        // create a default config for a standard 2 rom setup
        ets_printf("Writing default boot config.\r\n");
        // write new config sector
    }

    // try rom selected in the config, unless overriden by gpio/temp boot
    romToBoot = romconf->current_rom;

    // check valid rom number
    // gpio/temp boots will have already validated this
    if (romconf->current_rom >= romconf->count) {
        // if invalid rom selected try rom 0
        ets_printf("Invalid rom selected, defaulting to 0.\r\n");
    }

    // check rom is valid
    loadAddr = check_image(romconf->roms[romToBoot]);

    // check we have a good rom
    while (loadAddr == 0) {
        ets_printf("Rom %d at %x is bad.\r\n", romToBoot, romconf->roms[romToBoot]);
        // for normal mode try each previous rom
        // until we find a good one or run out
        updateConfig = 1;
        romToBoot--;
        if (romToBoot < 0) romToBoot = romconf->count - 1;
        if (romToBoot == romconf->current_rom) {
            // tried them all and all are bad!
            ets_printf("No good rom available.\r\n");
            return 0;
        }
        loadAddr = check_image(romconf->roms[romToBoot]);
    }

    // re-write config, if required
    if (updateConfig) {
        romconf->current_rom = romToBoot;

        SPIEraseSector(BOOT_CONFIG_SECTOR);
        SPIWrite(BOOT_CONFIG_SECTOR * SECTOR_SIZE, buffer, SECTOR_SIZE);
    }

    ets_printf("Booting rom %d at %x, load addr %x.\r\n", romToBoot, romconf->roms[romToBoot], loadAddr);
    // copy the loader to top of iram
    ets_memcpy((void*)_text_addr, _text_data, _text_len);
    // return address to load from
    return loadAddr;

}

很大一部分在作有效性判斷,簡化流程以下:函數

if(romconf->magic != BOOT_CONFIG_MAGIC) {
    return;
}
if(romconf->version != BOOT_CONFIG_VERSION) {
    return;
}
if(romconf->current_rom >= romconf->count) {
    return;
}

for(int i=0; i<romconf->count; i++) {
    loadAddr = check_image(romconf->roms[i]);
    if(loadAddr != 0) {
        break;
    }
}
if(loadAddr == 0) {
    return;
}

loader(loadAddr);

其中check_image主要是檢測了ESP8266固件自己的有效性。
ESP8266生成的bin文件中,實際上是包含了一些外部信息的,如flash模式,flash速度等,還用使用boot的版本,還有最主要的是生成文件的各段的信息。ESP8266有一些歷史版本的boot,因此格式也稍有區別。ui

加載固件

rboot-stage2a.c中:this

usercode* NOINLINE load_rom(uint32_t readpos) {
    
    uint8_t sectcount;
    uint8_t *writepos;
    uint32_t remaining;
    usercode* usercode;
    
    rom_header header;
    section_header section;
    
    // read rom header
    SPIRead(readpos, &header, sizeof(rom_header));
    readpos += sizeof(rom_header);

    // create function pointer for entry point
    usercode = header.entry;
    
    // copy all the sections
    for (sectcount = header.count; sectcount > 0; sectcount--) {
        
        // read section header
        SPIRead(readpos, &section, sizeof(section_header));
        readpos += sizeof(section_header);

        // get section address and length
        writepos = section.address;
        remaining = section.length;
        
        while (remaining > 0) {
            // work out how much to read, up to 16 bytes at a time
            uint32_t readlen = (remaining < READ_SIZE) ? remaining : READ_SIZE;
            // read the block
            SPIRead(readpos, writepos, readlen);
            readpos += readlen;
            // increment next write position
            writepos += readlen;
            // decrement remaining count
            remaining -= readlen;
        }
    }

    return usercode;
}

前面說到,ESP8266生成的bin文件中包含了各段的信息,包括段地址、大小和內容。load_rom即是遍歷bin文件中全部的段,從flash中讀取,並加載到對應地址中。而若是沒有Bootloader的話,這個工做是在ROM代碼中執行的。code

End

到這裏,rboot的加載固件流程就講完了。rboot還有很多特性,如GPIO選擇ROM,加載多個ROM等,這些功能不太經常使用,這裏就不打算說了。有須要的能夠在評論區留言,我也會視狀況繼續寫下去的。
那麼下一篇,就是寫zboot啦,做者說是基於rboot上作了改進,有了rboot的基礎後,解讀起來應該不會太難,你們一塊兒加油啊。orm

相關文章
相關標籤/搜索