回顾

上一篇说了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中(掉电保存)。这样做有几个目的:

  1. 知道使用哪个ROM区进行启动

  2. 可以配置ROM区地址(更灵活)

  3. 保存版本信息,使更易兼容

  4. 可以知道当前数据是否正确,有无损坏

    检查固件

    如果把uint32_t find_image(void)函数简化,是这样的:

    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,所以格式也稍有区别。

    加载固件

    rboot-stage2a.c中:

    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代码中执行的。

    End

    到这里,rboot的加载固件流程就讲完了。rboot还有不少特性,如GPIO选择ROM,加载多个ROM等,这些功能不太常用,这里就不打算说了。有需要的可以在评论区留言,我也会视情况继续写下去的。
    那么下一篇,就是写zboot啦,作者说是基于rboot上做了改进,有了rboot的基础后,解读起来应该不会太难,大家一起加油啊。



本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!