共计 4440 个字符,预计需要花费 12 分钟才能阅读完成。
ESP8266 Bootloader 开源代码解析之 rboot(一)
回顾
上一篇说了 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 中(掉电保留)。这样做有几个目标:
- 晓得应用哪个 ROM 区进行启动
- 能够配置 ROM 区地址(更灵便)
- 保留版本信息,使更易兼容
- 能够晓得以后数据是否正确,有无损坏
查看固件
如果把 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, §ion, 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 的根底后,解读起来应该不会太难,大家一起加油啊。