翻译[2]-探讨一种自定义esp32-c3引导加载程序(bootloader)的方式
探讨一种自定义esp32-c3引导加载程序(bootloader)的方式
- 原文地址: [https://danielmangum.com/posts/risc-v-bytes-exploring-custom-esp32-bootloader/]
- 原作者: Daniel Mangum
前言
我最近入手了一块ESP32-C3-DevKitC-02模块,按照我一贯的做法,立刻开始研究系统的启动方式以及Espressif提供的(相当不错的!)工具是如何工作的。在RISC-V Bytes系列中,我们通常使用QEMU,但接触到实物硬件后,感觉一切变得更加真实。在关于ESP32的第一篇文章中,我们将进行一些基本的设置,并研究一个简单的自定义引导加载程序。
上电
系统在供电时启动。因此,为了监控日志,您可能需要在打开串行端口后使用minicom等工具发出重置命令。将设备连接到您的机器后,您应该能在/dev/ttyUSB*下找到串行端口。在我的机器上是/dev/ttyUSB0。波特率为115200,这是minicom的默认值。
$ minicom -D /dev/ttyUSB0
如果我们然后按RST按钮,我们应该能看到输出。在我的模块上,预先编程了一个Rainmaker演示,它在串行端口上打印了一些ASCII艺术。
安装工具
Espressif 提供了工具,使得为任何 ESP32 模块构建项目变得更加简单。虽然不是严格必需的,但 Espressif 强烈推荐使用 esp-idf(ESP IOT 开发框架)。在仓库的根目录下,您会找到使用其中一个安装脚本的启动说明。像往常一样,我使用的是 Linux 机器,所以我使用了 install.sh 脚本。
$ ./install.sh
Detecting the Python interpreter
Checking "python3" ...
Python 3.8.10
"python3" has been detected
Checking Python compatibility
Installing ESP-IDF tools
Current system platform: linux-amd64
Selected targets are: esp32c2, esp32c6, esp32s2, esp32c3, esp32, esp32s3, esp32h4, esp32h2
Installing tools: xtensa-esp-elf-gdb, riscv32-esp-elf-gdb, xtensa-esp32-elf, xtensa-esp32s2-elf, xtensa-esp32s3-elf, riscv32-esp-elf, esp32ulp-elf, openocd-esp32, esp-rom-elfs
Skipping xtensa-esp-elf-gdb@12.1_20221002 (already installed)
Skipping riscv32-esp-elf-gdb@12.1_20221002 (already installed)
Skipping xtensa-esp32-elf@esp-12.2.0_20230208 (already installed)
Skipping xtensa-esp32s2-elf@esp-12.2.0_20230208 (already installed)
Skipping xtensa-esp32s3-elf@esp-12.2.0_20230208 (already installed)
Skipping riscv32-esp-elf@esp-12.2.0_20230208 (already installed)
Skipping esp32ulp-elf@2.35_20220830 (already installed)
Skipping openocd-esp32@v0.11.0-esp32-20221026 (already installed)
Skipping esp-rom-elfs@20230113 (already installed)
Installing Python environment and packages
...
Installing collected packages: esp-idf-monitor
Attempting uninstall: esp-idf-monitor
Found existing installation: esp-idf-monitor 1.0.1
Uninstalling esp-idf-monitor-1.0.1:
Successfully uninstalled esp-idf-monitor-1.0.1
Successfully installed esp-idf-monitor-1.0.0
All done! You can now run:
. ./export.sh
默认情况下,这将安装所有目标平台的工具链(或者如果它们已经存在,如上所示,将跳过安装)。它还会安装支持工具,如 idf.py 和 esptool。您可以查看 ~/.espressif 中安装的所有内容。
$ ls ~/.espressif/tools/
esp32ulp-elf/ esp-rom-elfs/ openocd-esp32/ riscv32-esp-elf/ riscv32-esp-elf-gdb/ xtensa-esp32-elf/ xtensa-esp32s2-elf/ xtensa-esp32s3-elf/ xtensa-esp-elf-gdb/
$ ls ~/.espressif/python_env/idf5.1_py3.8_env/bin/
activate allmodconfig doesitcache esptool.py menuconfig pip pyserial-miniterm savedefconfig
activate.csh allnoconfig esp-coredump futurize normalizer pip3 pyserial-ports setconfig
activate.fish allyesconfig espefuse.py genconfig oldconfig pip3.8 python tqdm
Activate.ps1 compote esp_rfc2217_server.py guiconfig olddefconfig __pycache__/ python3 west
alldefconfig defconfig espsecure.py listnewconfig
为了将必要的工具添加到您的 $PATH,可以执行相应的 export 脚本来实现。
$ . ./export.sh
Detecting the Python interpreter
Checking "python3" ...
Python 3.8.10
"python3" has been detected
Checking Python compatibility
Checking other ESP-IDF version.
Using a supported version of tool cmake found in PATH: 3.16.3.
However the recommended version is 3.24.0.
Adding ESP-IDF tools to PATH...
Using a supported version of tool cmake found in PATH: 3.16.3.
However the recommended version is 3.24.0.
Checking if Python packages are up to date...
Constraint file: /home/dan/.espressif/espidf.constraints.v5.1.txt
Requirement files:
- /home/dan/code/github.com/espressif/esp-idf/tools/requirements/requirements.core.txt
Python being checked: /home/dan/.espressif/python_env/idf5.1_py3.8_env/bin/python
Python requirements are satisfied.
Added the following directories to PATH:
/home/dan/code/github.com/espressif/esp-idf/components/esptool_py/esptool
/home/dan/code/github.com/espressif/esp-idf/components/espcoredump
/home/dan/code/github.com/espressif/esp-idf/components/partition_table
/home/dan/code/github.com/espressif/esp-idf/components/app_update
Done! You can now compile ESP-IDF projects.
Go to the project directory and run:
idf.py build
正如输出所示,我们现在已经准备好开始构建了!
启动流程
ESP32-C3 API指南中详细描述了引导过程。主要的要点是,引导是一个两阶段的过程,第一阶段引导加载程序存储在ROM中,无法修改,它负责加载第二阶段引导加载程序。第二阶段引导加载程序位于闪存内存的偏移量0x0处,但由第一阶段引导加载程序加载到RAM中。
模组内部结构 | 模组选型比较 |
---|---|
在深入探讨之前,了解ESP32-C3-DevKitC-02的各个组件很有帮助。ESP32-C3是片上系统(SoC),但位于ESP32-C3-WROOM-02模块内部。该模块围绕SoC配备了外围设备,如SPI闪存和PCB天线,使组合单元能够使用WiFi、蓝牙LE等功能。模块本身被开发套件PCB上的其他外围设备包围,如micro-USB端口和USB-to-UART桥,使得从我们的主机机器与之交互变得更加容易。
为了覆盖WROOM模块中与其并存的闪存中的第二阶段引导加载程序,我们需要与ESP32-C3通信,然后通过SPI总线与闪存进行通信。
esptool.py的串口通信协议
[https://en.wikipedia.org/wiki/Serial_Line_Internet_Protocol]
[https://docs.espressif.com/projects/esptool/en/latest/esp32c3/esptool/flasher-stub.html]
[https://docs.espressif.com/projects/esptool/en/latest/esp32c3/advanced-topics/serial-protocol.html]
在设置过程中安装的工具之一是esptool.py。尽管大多数文档使用idf.py,但它提供的许多命令只是对esptool.py的封装并传递必要的标志。ESP32-C3可以被配置为以“串行模式”启动,该模式实现了一个串行协议,支持各种命令,允许对闪存进行读写等操作。有趣的是,默认情况下esptool.py将加载一个实现了相同协议的stub引导加载程序,但它有一些优化和额外的功能。您可以通过向任何命令传递--no-stub
参数来选择跳过加载stub引导加载程序。
该串行协议基于串行线路互联网协议(SLIP)。文档提供了数据包格式的完整规范,但命令和响应的一般结构如下所示。
命令数据包
字节 | 名称 | 注释 |
---|---|---|
0 | 方向 | 对于请求始终为0x00 |
1 | 命令 | 命令标识符(见命令)。 |
2-3 | 大小 | 数据字段长度,以字节为单位。 |
4-7 | 校验和 | 数据字段部分的简单校验和(仅用于某些命令,见校验和)。 |
8-n | 数据 | 可变长度数据负载(0-65535字节,由大小参数指示)。具体用途取决于特定命令。 |
响应数据包
字节 | 名称 | 注释 |
---|---|---|
0 | 方向 | 对于响应始终为0x01 |
1 | 命令 | 与触发响应的请求包中的命令标识符相同的值 |
2-3 | 大小 | 数据字段的大小。至少包括状态字节的长度(2或4字节,见下文)。 |
4-7 | 值 | READ_REG命令使用的响应值(见下文)。其他情况下为零。 |
8-n | 数据 | 可变长度数据负载。长度由“大小”字段指示。 |
用于写入数据的命令序列遵循一个类似的模式,即一个开始命令,然后是一些数量的数据命令,最后是一个结束命令。这种模式用于加载stub引导加载程序,随后用于加载第二阶段引导加载程序。前者写入RAM,后者写入闪存。写入RAM的命令有:
- MEM_BEGIN (0x05)
- MEM_DATA (0x07)
- MEM_END (0x06)
MEM_END命令支持提供一个执行标志和RAM中的地址(每个都是32位字),如果设置了执行标志,芯片将从该地址开始执行指令。写入闪存的命令有:
- FLASH_BEGIN (0x02)
- FLASH_DATA (0x03)
- FLASH_END (0x04)
FLASH_END命令可以提供一个32位字,如果值为0,芯片将重启。
自定义二阶引导程序
[https://github.com/espressif/esp-idf/tree/master/examples/custom_bootloader/bootloader_override]
[https://github.com/espressif/esp-idf/tree/master/components/bootloader]
幸运的是,esp-idf仓库有一个关于如何覆盖第二阶段引导加载程序的示例。这个示例与默认的第二阶段引导加载程序非常相似,但它允许自定义在启动时打印的额外信息。
在运行README.md中的命令之前,我们需要指定我们正在针对哪个ESP32 SoC。
$ idf.py set-target esp32c3
Adding "set-target"'s dependency "fullclean" to list of commands with default set of options.
Executing action: fullclean
Executing action: set-target
Set Target to: esp32c3, new sdkconfig will be created.
...
-- Configuring done
-- Generating done
-- Build files have been written to: /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build
这将确保我们拥有必要的工具链组件,并设置正确的配置(sdkconfig)。它还将设置构建目录机制,然后我们可以进行操作。
$ idf.py build
Executing action: all (aliases: build)
Running cmake in directory /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build
Executing "cmake -G Ninja -DPYTHON_DEPS_CHECKED=1 -DPYTHON=/home/dan/.espressif/python_env/idf5.1_py3.8_env/bin/python -DESP_PLATFORM=1 -DCCACHE_ENABLE=0 /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override"...
-- IDF_TARGET is not set, guessed 'esp32c3' from sdkconfig '/home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/sdkconfig'
...
[848/849] Generating binary image from built executable
esptool.py v4.5.1
Creating esp32c3 image...
Merged 1 ELF section
Successfully created esp32c3 image.
Generated /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build/main.bin
[849/849] cd /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build/esp-idf/esptool_py && /home/dan/.espressif/python_env/idf5.1_py3.8_env/bin/python /home/dan/code/github.com/espressif/esp-idf/components/partition_table/check_sizes.py --offset 0x8000 partition --type app /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build/partition_table/partition-table.bin /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build/main.bin
main.bin binary size 0x28e00 bytes. Smallest app partition is 0x100000 bytes. 0xd7200 bytes (84%) free.
Project build complete. To flash, run this command:
/home/dan/.espressif/python_env/idf5.1_py3.8_env/bin/python ../../../components/esptool_py/esptool/esptool.py -p (PORT) -b 460800 --before default_reset --after hard_reset --chip esp32c3 write_flash --flash_mode dio --flash_size 2MB --flash_freq 80m 0x0 build/bootloader/bootloader.bin 0x8000 build/partition_table/partition-table.bin 0x10000 build/main.bin
or run 'idf.py -p (PORT) flash'
我们可以看到,它自动默认为esp32c3目标,构建了构件,并提供了用于刷新设备的命令。这两个命令选项展示了idf.py如何封装esptool,后者将使用write_flash命令将三个不同的二进制文件写入闪存中的不同位置。
- build/bootloader/bootloader.bin将被写入到0x0
- build/partition_table/partition-table.bin将被写入到0x8000
- build/main.bin将被写入到0x10000
我们将在下一节深入探讨这些二进制文件是什么,以及为什么它们会被写入到闪存中的这些偏移量,但首先让我们看看它的实际操作! - 注意:我们可以省略-p /dev/ttyUSB0,因为它是默认值。
$ idf.py flash
Executing action: flash
Serial port /dev/ttyUSB0
Connecting....
Detecting chip type... ESP32-C3
Running ninja in directory /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build
Executing "ninja flash"...
[1/5] cd /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build/esp-idf/esptool_py && /home/dan/.espressif/python_env/idf5.1_py3.8_env/bin/python /home/dan/code/github.com/espressif/esp-idf/components/partition_table/check_sizes.py --offset 0x8000 partition --type app /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build/partition_table/partition-table.bin /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build/main.bin
main.bin binary size 0x28e00 bytes. Smallest app partition is 0x100000 bytes. 0xd7200 bytes (84%) free.
[2/5] Performing build step for 'bootloader'
[1/1] cd /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build/bootloader/esp-idf/esptool_py && /home/dan/.espressif/python_env/idf5.1_py3.8_env/bin/python /home/dan/code/github.com/espressif/esp-idf/components/partition_table/check_sizes.py --offset 0x8000 bootloader 0x0 /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build/bootloader/bootloader.bin
Bootloader binary size 0x5080 bytes. 0x2f80 bytes (37%) free.
[2/3] cd /home/dan/code/github.com/espressif/esp-idf/components/esptool_py && /usr/bin/cmake -D IDF_PATH=/home/dan/code/github.com/espressif/esp-idf -D "SERIAL_TOOL=/home/dan/.espressif/python_env/idf5.1_py3.8_env/bin/python;;/home/dan/code/github.com/espressif/esp-idf/components/esptool_py/esptool/esptool.py;--chip;esp32c3" -D "SERIAL_TOOL_ARGS=--before=default_reset;--after=hard_reset;write_flash;@flash_args" -D WORKING_DIRECTORY=/home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build -P /home/dan/code/github.com/espressif/esp-idf/components/esptool_py/run_serial_tool.cmake
esptool esp32c3 -p /dev/ttyUSB0 -b 460800 --before=default_reset --after=hard_reset write_flash --flash_mode dio --flash_freq 80m --flash_size 2MB 0x0 bootloader/bootloader.bin 0x10000 main.bin 0x8000 partition_table/partition-table.bin
esptool.py v4.5.1
Serial port /dev/ttyUSB0
Connecting....
Chip is ESP32-C3 (revision v0.3)
Features: WiFi, BLE
Crystal is 40MHz
MAC: 58:cf:79:16:7d:a0
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 460800
Changed.
Configuring flash size...
Flash will be erased from 0x00000000 to 0x00005fff...
Flash will be erased from 0x00010000 to 0x00038fff...
Flash will be erased from 0x00008000 to 0x00008fff...
Compressed 20608 bytes to 12655...
Writing at 0x00000000... (100 %)
Wrote 20608 bytes (12655 compressed) at 0x00000000 in 0.7 seconds (effective 244.4 kbit/s)...
Hash of data verified.
Compressed 167424 bytes to 88511...
Writing at 0x00010000... (16 %)
Writing at 0x0001a51f... (33 %)
Writing at 0x0002104c... (50 %)
Writing at 0x00028442... (66 %)
Writing at 0x0002ec04... (83 %)
Writing at 0x00035e5c... (100 %)
Wrote 167424 bytes (88511 compressed) at 0x00010000 in 2.8 seconds (effective 477.3 kbit/s)...
Hash of data verified.
Compressed 3072 bytes to 103...
Writing at 0x00008000... (100 %)
Wrote 3072 bytes (103 compressed) at 0x00008000 in 0.1 seconds (effective 295.2 kbit/s)...
Hash of data verified.
Leaving...
Hard resetting via RTS pin...
Done
这一步中的大部分逻辑可以在write_flash命令的源代码中找到,但大致步骤如下:
- 加载stub引导加载程序。
...
正在上传stub...
正在运行stub...
Stub正在运行...
...
- 擦除闪存部分。
...
正在配置闪存大小...
将从0x00000000擦除闪存至0x00005fff...
将从0x00010000擦除闪存至0x00038fff...
将从0x00008000擦除闪存至0x00008fff...
...
- 将第二阶段引导加载程序写入闪存。
...
已将20608字节压缩至12655...
正在写入0x00000000... (100 %)
在0.7秒内在0x00000000处写入了20608字节(压缩后12655字节),有效速度为244.4 kbit/s...
数据哈希已验证。
...
- 将应用程序写入闪存。
...
已将167424字节压缩至88511...
正在写入0x00010000... (16 %)
正在写入0x0001a51f... (33 %)
正在写入0x0002104c... (50 %)
正在写入0x00028442... (66 %)
正在写入0x0002ec04... (83 %)
正在写入0x00035e5c... (100 %)
在2.8秒内在0x00010000处写入了167424字节(压缩后88511字节),有效速度为477.3 kbit/s...
数据哈希已验证。
...
- 将分区表写入闪存。
...
已将3072字节压缩至103...
正在写入0x00008000... (100 %)
在0.1秒内在0x00008000处写入了3072字节(压缩后103字节),有效速度为295.2 kbit/s...
数据哈希已验证。
...
- 复位芯片。
...
正在离开...
通过RTS引脚进行硬件复位...
完成
为了查看现在启动时会发生什么,我们可以再次通过minicom连接并按下RST按钮。
$ minicom -D /dev/ttyUSB0
ESP-ROM:esp32c3-api1-20210207
Build:Feb 7 2021
rst:0x1 (POWERON),boot:0xc (SPI_FAST_FLASH_BOOT)
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fcd5820,len:0x1754
load:0x403cc710,len:0x970
load:0x403ce710,len:0x2f68
entry 0x403cc710
I (30) boot: ESP-IDF 4f0769d2ed 2nd stage bootloader
I (30) boot: compile time Apr 8 2023 18:51:23
I (30) boot: chip revision: v0.3
I (34) boot.esp32c3: SPI Speed : 80MHz
I (38) boot.esp32c3: SPI Mode : DIO
I (43) boot.esp32c3: SPI Flash Size : 2MB
I (48) boot: Enabling RNG early entropy source...
I (53) boot: Partition Table:
I (57) boot: ## Label Usage Type ST Offset Length
I (64) boot: 0 nvs WiFi data 01 02 00009000 00006000
I (71) boot: 1 phy_init RF data 01 01 0000f000 00001000
I (79) boot: 2 factory factory app 00 00 00010000 00100000
I (86) boot: End of partition table
[boot] Custom bootloader message defined in the KConfig file.
I (96) esp_image: segment 0: paddr=00010020 vaddr=3c020020 size=08480h ( 33920) map
I (110) esp_image: segment 1: paddr=000184a8 vaddr=3fc8aa00 size=01110h ( 4368) load
I (114) esp_image: segment 2: paddr=000195c0 vaddr=40380000 size=06a58h ( 27224) load
I (126) esp_image: segment 3: paddr=00020020 vaddr=42000020 size=15018h ( 86040) map
I (143) esp_image: segment 4: paddr=00035040 vaddr=40386a58 size=03d9ch ( 15772) load
I (149) boot: Loaded app from partition at offset 0x10000
I (149) boot: Disabling RNG early entropy source...
I (163) cpu_start: Pro cpu up.
I (172) cpu_start: Pro cpu start user code
I (172) cpu_start: cpu freq: 160000000 Hz
I (172) cpu_start: Application information:
I (175) cpu_start: Project name: main
I (180) cpu_start: App version: 4f0769d2ed
I (185) cpu_start: Compile time: Apr 8 2023 18:51:16
I (191) cpu_start: ELF file SHA256: 3916cd87115c6efe...
I (197) cpu_start: ESP-IDF: 4f0769d2ed
I (203) cpu_start: Min chip rev: v0.3
I (207) cpu_start: Max chip rev: v0.99
I (212) cpu_start: Chip rev: v0.3
I (217) heap_init: Initializing. RAM available for dynamic allocation:
I (224) heap_init: At 3FC8C940 len 0004FDD0 (319 KiB): DRAM
I (230) heap_init: At 3FCDC710 len 00002950 (10 KiB): STACK/DRAM
I (237) heap_init: At 50000020 len 00001FE0 (7 KiB): RTCRAM
I (244) spi_flash: detected chip: generic
I (248) spi_flash: flash io: dio
W (252) spi_flash: Detected size(4096k) larger than the size in the binary image header(2048k). Using the size in the binary image header.
I (265) sleep: Configure to isolate all GPIO pins in sleep state
I (272) sleep: Enable automatic switching of GPIO sleep configuration
I (279) app_start: Starting scheduler on CPU0
I (284) main_task: Started on CPU0
I (284) main_task: Calling app_main()
Application started!
I (294) main_task: Returned from app_main()
如自定义信息所示,第二阶段引导加载程序已成功加载和执行。
[boot] Custom bootloader message defined in the KConfig file.
我们还可以看到引导加载程序跳转到的简单应用程序的信息。
Application started!
刨根究底
现在我们已经运行了自定义引导加载程序的示例,让我们探索一下二进制文件,以了解幕后发生了什么。第二阶段引导加载程序的入口点是 call_start_cpu0(void)
。
examples/custom_bootloader/bootloader_override/bootloader_components/main/bootloader_start.c
/*
* We arrive here after the ROM bootloader finished loading this second stage bootloader from flash.
* The hardware is mostly uninitialized, flash cache is down and the app CPU is in reset.
* We do have a stack, so we can do the initialization in C.
*/
void __attribute__((noreturn)) call_start_cpu0(void)
{
// 1. Hardware initialization
if (bootloader_init() != ESP_OK) {
bootloader_reset();
}
#ifdef CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP
// If this boot is a wake up from the deep sleep then go to the short way,
// try to load the application which worked before deep sleep.
// It skips a lot of checks due to it was done before (while first boot).
bootloader_utility_load_boot_image_from_deep_sleep();
// If it is not successful try to load an application as usual.
#endif
// 2. Select the number of boot partition
bootloader_state_t bs = {0};
int boot_index = select_partition_number(&bs);
if (boot_index == INVALID_INDEX) {
bootloader_reset();
}
// 2.1 Print a custom message!
esp_rom_printf("[%s] %s\n", TAG, CONFIG_EXAMPLE_BOOTLOADER_WELCOME_MESSAGE);
// 3. Load the app image for booting
bootloader_utility_load_boot_image(&bs, boot_index);
}
提供的注释有助于理解发生了什么,我们稍后会探索它们,但第一阶段引导加载程序是如何知道跳转到这里的呢?我们的自定义引导加载程序重用了与默认第二阶段引导加载程序相同的链接器脚本,该脚本在 call_start_cpu0
函数的位置定义了入口点。
components/bootloader/subproject/main/ld/esp32c3/bootloader.ld
/* Default entry point: */
ENTRY(call_start_cpu0);
它还定义了一些用于指令内存(IRAM)和数据内存(DRAM)的内存区域。
components/bootloader/subproject/main/ld/esp32c3/bootloader.ld
MEMORY
{
iram_seg (RWX) : org = bootloader_iram_seg_start, len = bootloader_iram_seg_len
iram_loader_seg (RWX) : org = bootloader_iram_loader_seg_start, len = bootloader_iram_loader_seg_len
dram_seg (RW) : org = bootloader_dram_seg_start, len = bootloader_dram_seg_len
}
入口点被加载到IRAM段的开始处。
components/bootloader/subproject/main/ld/esp32c3/bootloader.ld
.iram.text :
{
. = ALIGN (16);
*(.entry.text)
*(.init.literal)
*(.init)
} > iram_seg
我们可以通过检查为引导加载程序映像构建的ELF文件来看到这种布局的效果。如果您成功地将所有安装的工具添加到您的路径中,您应该能够使用 riscv32-esp-elf-objdump。
$ riscv32-esp-elf-objdump -h build/bootloader/bootloader.elf
build/bootloader/bootloader.elf: file format elf32-littleriscv
Sections:
Idx Name Size VMA LMA File off Algn
0 .iram_loader.text 00002f66 403ce710 403ce710 00003710 2**1
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .iram.text 00000000 403cc710 403cc710 00006676 2**0
CONTENTS
2 .dram0.bss 00000110 3fcd5710 3fcd5710 00000710 2**2
ALLOC
3 .dram0.data 00000004 3fcd5820 3fcd5820 00000820 2**2
CONTENTS, ALLOC, LOAD, DATA
4 .dram0.rodata 00001750 3fcd5824 3fcd5824 00000824 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .iram.text 0000096e 403cc710 403cc710 00002710 2**1
CONTENTS, ALLOC, LOAD, READONLY, CODE
6 .debug_info 000229fa 00000000 00000000 00006676 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
7 .debug_abbrev 000048b6 00000000 00000000 00029070 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
8 .debug_loc 000075d6 00000000 00000000 0002d926 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
9 .debug_aranges 00000808 00000000 00000000 00034efc 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
10 .debug_ranges 000015b8 00000000 00000000 00035704 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
11 .debug_line 00010e82 00000000 00000000 00036cbc 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
12 .debug_str 0000a1e0 00000000 00000000 00047b3e 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
13 .comment 0000002f 00000000 00000000 00051d1e 2**0
CONTENTS, READONLY
14 .riscv.attributes 0000003f 00000000 00000000 00051d4d 2**0
CONTENTS, READONLY
15 .debug_frame 000014bc 00000000 00000000 00051d8c 2**2
CONTENTS, READONLY, DEBUGGING, OCTETS
- 注意:-h标志表示我们只想获取节头信息。
我们看到两个.iram.text
节,它们看起来除了Size和Algn(对齐)之外都相同。因为所有的.iram.text
符号都是16位对齐的(ALIGN (16)),所以1字节对齐的节(2**0或2^0 = 1
)是空的,所有的数据都在2字节对齐的((2**0或2^1 = 2
)节中。两个节具有相同的虚拟内存地址(VMA),这是有意义的,因为第一个节的大小是0。我们可以跳转到.iram.text
节,看看我们的call_start_cpu0
函数是否确实存在。
$ riscv32-esp-elf-objdump -D -j .iram.text build/bootloader/bootloader.elf | head -46
build/bootloader/bootloader.elf: file format elf32-littleriscv
Disassembly of section .iram.text:
403cc710 <call_start_cpu0>:
403cc710: 7171 addi sp,sp,-176
403cc712: d706 sw ra,172(sp)
403cc714: d522 sw s0,168(sp)
403cc716: d326 sw s1,164(sp)
403cc718: 2895 jal 403cc78c <bootloader_init>
403cc71a: c119 beqz a0,403cc720 <call_start_cpu0+0x10>
403cc71c: 55a030ef jal ra,403cfc76 <bootloader_reset>
403cc720: 0a000613 li a2,160
403cc724: 4581 li a1,0
403cc726: 850a mv a0,sp
403cc728: ffc34097 auipc ra,0xffc34
403cc72c: c2c080e7 jalr -980(ra) # 40000354 <memset>
403cc730: 850a mv a0,sp
403cc732: 19a030ef jal ra,403cf8cc <bootloader_utility_load_partition_table>
403cc736: 3fcd64b7 lui s1,0x3fcd6
403cc73a: ed19 bnez a0,403cc758 <call_start_cpu0+0x48>
403cc73c: 688020ef jal ra,403cedc4 <esp_log_early_timestamp>
403cc740: 85aa mv a1,a0
403cc742: 3fcd6537 lui a0,0x3fcd6
403cc746: 86c48613 addi a2,s1,-1940 # 3fcd586c <_data_end+0x48>
403cc74a: 87450513 addi a0,a0,-1932 # 3fcd5874 <_data_end+0x50>
403cc74e: ffc34097 auipc ra,0xffc34
403cc752: 8f2080e7 jalr -1806(ra) # 40000040 <esp_rom_printf>
403cc756: b7d9 j 403cc71c <call_start_cpu0+0xc>
403cc758: 850a mv a0,sp
403cc75a: 3a0030ef jal ra,403cfafa <bootloader_utility_get_selected_boot_partition>
403cc75e: f9d00793 li a5,-99
403cc762: 842a mv s0,a0
403cc764: faf50ce3 beq a0,a5,403cc71c <call_start_cpu0+0xc>
403cc768: 3fcd6637 lui a2,0x3fcd6
403cc76c: 3fcd6537 lui a0,0x3fcd6
403cc770: 86c48593 addi a1,s1,-1940
403cc774: 8a860613 addi a2,a2,-1880 # 3fcd58a8 <_data_end+0x84>
403cc778: 8e050513 addi a0,a0,-1824 # 3fcd58e0 <_data_end+0xbc>
403cc77c: ffc34097 auipc ra,0xffc34
403cc780: 8c4080e7 jalr -1852(ra) # 40000040 <esp_rom_printf>
403cc784: 85a2 mv a1,s0
403cc786: 850a mv a0,sp
403cc788: 50a030ef jal ra,403cfc92 <bootloader_utility_load_boot_image>
- 注意:-D标志表示我们想要反汇编所有节的内容。-j .iram.text表示我们只想要.iram.text节的内容。
[https://danielmangum.com/posts/risc-v-bytes-caller-callee-registers/]
在这里,我们看到熟悉的函数序言,它包括增加我们的堆栈(addi sp,sp,-176)并将我们的被调用者保存的寄存器(s0, s1)存储在上面,然后继续通过调用引导加载程序实用函数的各种步骤。bootloader_init()的实现因SoC而异,但高级步骤非常相似。下面是esp32c3的实现。
components/bootloader_support/src/esp32c3/bootloader_esp32c3.c
esp_err_t bootloader_init(void)
{
esp_err_t ret = ESP_OK;
bootloader_hardware_init();
bootloader_ana_reset_config();
bootloader_super_wdt_auto_feed();
// In RAM_APP, memory will be initialized in `call_start_cpu0`
#if !CONFIG_APP_BUILD_TYPE_RAM
// protect memory region
bootloader_init_mem();
/* check that static RAM is after the stack */
assert(&_bss_start <= &_bss_end);
assert(&_data_start <= &_data_end);
// clear bss section
bootloader_clear_bss_section();
#endif // !CONFIG_APP_BUILD_TYPE_RAM
// init eFuse virtual mode (read eFuses to RAM)
#ifdef CONFIG_EFUSE_VIRTUAL
ESP_LOGW(TAG, "eFuse virtual mode is enabled. If Secure boot or Flash encryption is enabled then it does not provide any security. FOR TESTING ONLY!");
#ifndef CONFIG_EFUSE_VIRTUAL_KEEP_IN_FLASH
esp_efuse_init_virtual_mode_in_ram();
#endif
#endif
// config clock
bootloader_clock_configure();
// initialize console, from now on, we can use esp_log
bootloader_console_init();
/* print 2nd bootloader banner */
bootloader_print_banner();
#if !CONFIG_APP_BUILD_TYPE_PURE_RAM_APP
//init cache hal
cache_hal_init();
//init mmu
mmu_hal_init();
// update flash ID
bootloader_flash_update_id();
// Check and run XMC startup flow
if ((ret = bootloader_flash_xmc_startup()) != ESP_OK) {
ESP_LOGE(TAG, "failed when running XMC startup flow, reboot!");
return ret;
}
#if !CONFIG_APP_BUILD_TYPE_RAM
// read bootloader header
if ((ret = bootloader_read_bootloader_header()) != ESP_OK) {
return ret;
}
// read chip revision and check if it's compatible to bootloader
if ((ret = bootloader_check_bootloader_validity()) != ESP_OK) {
return ret;
}
#endif //#if !CONFIG_APP_BUILD_TYPE_RAM
// initialize spi flash
if ((ret = bootloader_init_spi_flash()) != ESP_OK) {
return ret;
}
#endif // !CONFIG_APP_BUILD_TYPE_PURE_RAM_APP
// check whether a WDT reset happend
bootloader_check_wdt_reset();
// config WDT
bootloader_config_wdt();
// enable RNG early entropy source
bootloader_enable_random();
return ret;
}
如果所有初始化都成功(即bootloader_init()
返回0),我们使用beqz跳过对bootloader_reset()
的调用,并继续加载应用程序。
403cc71a: c119 beqz a0,403cc720 <call_start_cpu0+0x10>
403cc71c: 55a030ef jal ra,403cfc76 <bootloader_reset>
403cc720: 0a000613 li a2,160
[https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32c3/api-guides/partition-tables.html]
除了引导加载程序和应用程序二进制文件,我们还看到构建了一个分区表并将其加载到闪存中(它是最后一项,大小仅为3072字节)。分区表通知引导加载程序各种数据在闪存中的位置。当重置CPU时,我们在引导加载程序日志中看到了其内容的可视化表示。
I (53) boot: Partition Table:
I (57) boot: ## Label Usage Type ST Offset Length
I (64) boot: 0 nvs WiFi data 01 02 00009000 00006000
I (71) boot: 1 phy_init RF data 01 01 0000f000 00001000
I (79) boot: 2 factory factory app 00 00 00010000 00100000
I (86) boot: End of partition table
在这种情况下,我们正在加载工厂分区,我们的应用程序就是被写入到这个分区中的(0x00010000
)。这个过程中有许多步骤,但序列对应于以下函数调用。
- void bootloader_utility_load_boot_image(const bootloader_state_t *bs, int start_index): 确定所需映像存在的地方,如果找不到,则经历一系列备用选项。
- static void load_image(const esp_image_metadata_t *image_data): 将加载的段复制到RAM中,并为映射的段设置缓存。
- static void unpack_load_app(const esp_image_metadata_t *data): 为MMU配置映射。
最后一步实际上是启动应用程序,这是通过在entry_addr定义一个入口符号,然后调用它来完成的。
components/bootloader_support/src/bootloader_utility.c
static void set_cache_and_start_app(
uint32_t drom_addr,
uint32_t drom_load_addr,
uint32_t drom_size,
uint32_t irom_addr,
uint32_t irom_load_addr,
uint32_t irom_size,
uint32_t entry_addr)
{
...
ESP_LOGD(TAG, "start: 0x%08"PRIx32, entry_addr);
bootloader_atexit();
typedef void (*entry_t)(void) __attribute__((noreturn));
entry_t entry = ((entry_t) entry_addr);
// TODO: we have used quite a bit of stack at this point.
// use "movsp" instruction to reset stack back to where ROM stack starts.
(*entry)();
}
总结
[https://github.com/hasheddan/]
[https://github.com/hasheddan/HashedDan.github.io/tree/master/posts/risc-v-bytes-exploring-custom-esp32-bootloader]
当您想了解软件和硬件是如何通信的时候,Bootloaders(引导加载程序)是一个很好的切入点。虽然您可能永远不需要自己修改引导加载程序,但了解其工作原理对于理解在更高层次上所做的更改如何最终转化为机器指令是非常有用的。
和往常一样,这些帖子旨在为那些对学习更多关于RISC-V和底层软件的人提供有用的资源。如果我能更好地达到这个目标,或者您有任何问题或评论,请随时通过Twitter上的@hasheddan或Mastodon上的@hasheddan@types.pl给我发消息!
原文
About
I recently acquired an ESP32-C3-DevKitC-02 module, and, as I tend to do, jumped right into reading about how the system boots and how the (pretty good!) tooling Espressif offers works. We have typically used QEMU in the RISC-V Bytes series, but getting our hands on physical hardware starts to make things feel a bit more real. In this first post on the ESP32, we’ll do some basic setup and look at a simple custom bootloader.
Booting Up
The system boots when power is supplied. Because of this, to monitor the logs, you’ll likely need to issue a reset after opening the serial port with a tool such as minicom. With the device connected to your machine, you should be able to find the serial port under /dev/ttyUSB*. On my machine it was /dev/ttyUSB0. The baud rate is 115200, which is the default for minicom.
$ minicom -D /dev/ttyUSB0
If we then press the RST button, we should see output. Before overwriting, a Rainmaker demo was pre-programmed on my module, and it printed some ASCII art over the serial port.
Installing Tools
Espressif provides tooling that makes building projects for any ESP32 module much simpler. Though certainly not strictly required, Espressif highly recommends the use of the esp-idf (ESP IOT Development Framework). In the root of the repository, you’ll find instructions to get started with one of the install scripts. As usual, I am on a Linux machine, so I used the install.sh script.
$ ./install.sh
Detecting the Python interpreter
Checking "python3" ...
Python 3.8.10
"python3" has been detected
Checking Python compatibility
Installing ESP-IDF tools
Current system platform: linux-amd64
Selected targets are: esp32c2, esp32c6, esp32s2, esp32c3, esp32, esp32s3, esp32h4, esp32h2
Installing tools: xtensa-esp-elf-gdb, riscv32-esp-elf-gdb, xtensa-esp32-elf, xtensa-esp32s2-elf, xtensa-esp32s3-elf, riscv32-esp-elf, esp32ulp-elf, openocd-esp32, esp-rom-elfs
Skipping xtensa-esp-elf-gdb@12.1_20221002 (already installed)
Skipping riscv32-esp-elf-gdb@12.1_20221002 (already installed)
Skipping xtensa-esp32-elf@esp-12.2.0_20230208 (already installed)
Skipping xtensa-esp32s2-elf@esp-12.2.0_20230208 (already installed)
Skipping xtensa-esp32s3-elf@esp-12.2.0_20230208 (already installed)
Skipping riscv32-esp-elf@esp-12.2.0_20230208 (already installed)
Skipping esp32ulp-elf@2.35_20220830 (already installed)
Skipping openocd-esp32@v0.11.0-esp32-20221026 (already installed)
Skipping esp-rom-elfs@20230113 (already installed)
Installing Python environment and packages
...
Installing collected packages: esp-idf-monitor
Attempting uninstall: esp-idf-monitor
Found existing installation: esp-idf-monitor 1.0.1
Uninstalling esp-idf-monitor-1.0.1:
Successfully uninstalled esp-idf-monitor-1.0.1
Successfully installed esp-idf-monitor-1.0.0
All done! You can now run:
. ./export.sh
By default, this will install the toolchains for all targets (or skip if they are already present as shown above). It will also install supporting tools, such as idf.py and esptool. You can take a look at everything installed in ~/.espressif.
$ ls ~/.espressif/tools/
esp32ulp-elf/ esp-rom-elfs/ openocd-esp32/ riscv32-esp-elf/ riscv32-esp-elf-gdb/ xtensa-esp32-elf/ xtensa-esp32s2-elf/ xtensa-esp32s3-elf/ xtensa-esp-elf-gdb/
$ ls ~/.espressif/python_env/idf5.1_py3.8_env/bin/
activate allmodconfig doesitcache esptool.py menuconfig pip pyserial-miniterm savedefconfig
activate.csh allnoconfig esp-coredump futurize normalizer pip3 pyserial-ports setconfig
activate.fish allyesconfig espefuse.py genconfig oldconfig pip3.8 python tqdm
Activate.ps1 compote esp_rfc2217_server.py guiconfig olddefconfig __pycache__/ python3 west
alldefconfig defconfig espsecure.py listnewconfig
To add the necessary tools to your $PATH, the corresponding export script can be sourced.
$ . ./export.sh
Detecting the Python interpreter
Checking "python3" ...
Python 3.8.10
"python3" has been detected
Checking Python compatibility
Checking other ESP-IDF version.
Using a supported version of tool cmake found in PATH: 3.16.3.
However the recommended version is 3.24.0.
Adding ESP-IDF tools to PATH...
Using a supported version of tool cmake found in PATH: 3.16.3.
However the recommended version is 3.24.0.
Checking if Python packages are up to date...
Constraint file: /home/dan/.espressif/espidf.constraints.v5.1.txt
Requirement files:
- /home/dan/code/github.com/espressif/esp-idf/tools/requirements/requirements.core.txt
Python being checked: /home/dan/.espressif/python_env/idf5.1_py3.8_env/bin/python
Python requirements are satisfied.
Added the following directories to PATH:
/home/dan/code/github.com/espressif/esp-idf/components/esptool_py/esptool
/home/dan/code/github.com/espressif/esp-idf/components/espcoredump
/home/dan/code/github.com/espressif/esp-idf/components/partition_table
/home/dan/code/github.com/espressif/esp-idf/components/app_update
Done! You can now compile ESP-IDF projects.
Go to the project directory and run:
idf.py build
As indicated in the output, we are now ready to start building!
The Boot Process
The boot process is described in detail in the ESP32-C3 API Guide. The main takeaway is that booting is a two stage process, where the first stage bootloader, which is stored in ROM and cannot be modified, loads the second stage one. The second stage bootloader lives in flash memory at offset 0x0, but is loaded into RAM by the first stage bootloader.
模组内部结构 | 模组选型比较 |
---|---|
Before diving deeper, it is useful to understand the various components of the ESP32-C3-DevKitC-02. The ESP32-C3 is the system-on-chip (SoC), but lives inside of the ESP32-C3-WROOM-02 module. The module surrounds the SoC with peripherals, such as SPI flash and a PCB Antenna, enabling the combined unit to utilize WiFi, Bluetooth LE, and more. The module itself is surrounded by other peripherals on the development kit PCB, such as the micro-USB port and USB-to-UART bridge, making it much easier to interact with from our host machine.
In order to overwrite the second stage bootloader in flash, we’ll need to communicate with the ESP32-C3, which will then talk over the SPI bus to the flash that lives beside it in the WROOM module.
The Serial Protocol
[https://en.wikipedia.org/wiki/Serial_Line_Internet_Protocol]
[https://docs.espressif.com/projects/esptool/en/latest/esp32c3/esptool/flasher-stub.html]
[https://docs.espressif.com/projects/esptool/en/latest/esp32c3/advanced-topics/serial-protocol.html]
One of the tools installed during setup was esptool.py. Though most of the documentation uses idf.py, many of the commands it offers are just wrapping esptool.py and passing necessary flags. The ESP32-C3 can be configured to boot in “serial mode”, which implements a serial protocol with support for a variety of commands that allow for operations such as reading and writing to flash. Interestingly, by default esptool.py will load a stub bootloader that implements the same protocol, but has some optimizations and additional features. You can choose to skip loading the stub bootloader by passing the --no-stub
argument to any command.
The serial protocol is based on the Serial Line Internet Protocol. The documentation provides the full specification for packet format, but the general structure for commands and responses are reproduced below.
Command Packets
Byte | Name | Comment |
---|---|---|
0 | Direction | Always 0x00 for requests |
1 | Command | Command identifier (see Commands). |
2-3 | Size | Length of Data field, in bytes. |
4-7 | Checksum | Simple checksum of part of the data field (only used for some commands, see Checksum). |
8..n | Data | Variable length data payload (0-65535 bytes, as indicated by Size parameter). Usage depends on specific command. |
Response Packets
Byte | Name | Comment |
---|---|---|
0 | Direction | Always 0x01 for responses |
1 | Command | Same value as Command identifier in the request packet that triggered the response |
2-3 | Size | Size of data field. At least the length of the Status Bytes (2 or 4 bytes, see below). |
4-7 | Value | Response value used by READ_REG command (see below). Zero otherwise. |
8..n | Data | Variable length data payload. Length indicated by “Size” field. |
Command sequences used for writing data follow a similar pattern of a single begin command, followed by some number of data commands, then a single end command. This pattern is used to both load the stub bootloader and, subsequently, load the second stage bootloader. The former is written to RAM, while the latter is written to flash. The commands for writing to RAM are:
- MEM_BEGIN (0x05)
- MEM_DATA (0x07)
- MEM_END (0x06)
The MEM_END command supports supplying an execute flag and an address in RAM (each 32-bit words) that the chip will begin executing instructions at if execute flag is set. The commands for writing to flash are:
- FLASH_BEGIN (0x02)
- FLASH_DATA (0x03)
- FLASH_END (0x04)
The FLASH_END command can supply a single 32-bit word and if the value is 0 the chip will reboot.
Overriding the Second Stage Bootloader
[https://github.com/espressif/esp-idf/tree/master/examples/custom_bootloader/bootloader_override]
[https://github.com/espressif/esp-idf/tree/master/components/bootloader]
Fortunately, the esp-idf repo has an example of how the second stage bootloader can be overridden. It is quite similar to the default second stage bootloader, but it allows for customizing an additional message that is printed on startup.
Prior to running the commands in the README.md, we need to specify which ESP32 SoC we are targeting.
$ idf.py set-target esp32c3
Adding "set-target"'s dependency "fullclean" to list of commands with default set of options.
Executing action: fullclean
Executing action: set-target
Set Target to: esp32c3, new sdkconfig will be created.
...
-- Configuring done
-- Generating done
-- Build files have been written to: /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build
This will ensure that we have the necessary toolchain components and setup the proper configuration (sdkconfig). It will also setup the build directory machinery, which we can then exercise.
$ idf.py build
Executing action: all (aliases: build)
Running cmake in directory /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build
Executing "cmake -G Ninja -DPYTHON_DEPS_CHECKED=1 -DPYTHON=/home/dan/.espressif/python_env/idf5.1_py3.8_env/bin/python -DESP_PLATFORM=1 -DCCACHE_ENABLE=0 /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override"...
-- IDF_TARGET is not set, guessed 'esp32c3' from sdkconfig '/home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/sdkconfig'
...
[848/849] Generating binary image from built executable
esptool.py v4.5.1
Creating esp32c3 image...
Merged 1 ELF section
Successfully created esp32c3 image.
Generated /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build/main.bin
[849/849] cd /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build/esp-idf/esptool_py && /home/dan/.espressif/python_env/idf5.1_py3.8_env/bin/python /home/dan/code/github.com/espressif/esp-idf/components/partition_table/check_sizes.py --offset 0x8000 partition --type app /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build/partition_table/partition-table.bin /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build/main.bin
main.bin binary size 0x28e00 bytes. Smallest app partition is 0x100000 bytes. 0xd7200 bytes (84%) free.
Project build complete. To flash, run this command:
/home/dan/.espressif/python_env/idf5.1_py3.8_env/bin/python ../../../components/esptool_py/esptool/esptool.py -p (PORT) -b 460800 --before default_reset --after hard_reset --chip esp32c3 write_flash --flash_mode dio --flash_size 2MB --flash_freq 80m 0x0 build/bootloader/bootloader.bin 0x8000 build/partition_table/partition-table.bin 0x10000 build/main.bin
or run 'idf.py -p (PORT) flash'
We can see that it automatically defaulted to the esp32c3 target, built artifacts, and provided the command to be used to flash the device. The two command options demonstrate how idf.py wraps esptool, which will write three different binaries to various locations in flash with the write_flash command.
- build/bootloader/bootloader.bin will be written to 0x0
- build/partition_table/partition-table.bin will be written to 0x8000
- build/main.bin will be written to 0x10000
We’ll dig a little deeper into what the binaries are, as well as why they are being written to those offsets in flash in the next section, but first let’s see it in action!
- NOTE: we can leave off -p /dev/ttyUSB0 as it is the default.
$ idf.py flash
Executing action: flash
Serial port /dev/ttyUSB0
Connecting....
Detecting chip type... ESP32-C3
Running ninja in directory /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build
Executing "ninja flash"...
[1/5] cd /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build/esp-idf/esptool_py && /home/dan/.espressif/python_env/idf5.1_py3.8_env/bin/python /home/dan/code/github.com/espressif/esp-idf/components/partition_table/check_sizes.py --offset 0x8000 partition --type app /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build/partition_table/partition-table.bin /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build/main.bin
main.bin binary size 0x28e00 bytes. Smallest app partition is 0x100000 bytes. 0xd7200 bytes (84%) free.
[2/5] Performing build step for 'bootloader'
[1/1] cd /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build/bootloader/esp-idf/esptool_py && /home/dan/.espressif/python_env/idf5.1_py3.8_env/bin/python /home/dan/code/github.com/espressif/esp-idf/components/partition_table/check_sizes.py --offset 0x8000 bootloader 0x0 /home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build/bootloader/bootloader.bin
Bootloader binary size 0x5080 bytes. 0x2f80 bytes (37%) free.
[2/3] cd /home/dan/code/github.com/espressif/esp-idf/components/esptool_py && /usr/bin/cmake -D IDF_PATH=/home/dan/code/github.com/espressif/esp-idf -D "SERIAL_TOOL=/home/dan/.espressif/python_env/idf5.1_py3.8_env/bin/python;;/home/dan/code/github.com/espressif/esp-idf/components/esptool_py/esptool/esptool.py;--chip;esp32c3" -D "SERIAL_TOOL_ARGS=--before=default_reset;--after=hard_reset;write_flash;@flash_args" -D WORKING_DIRECTORY=/home/dan/code/github.com/espressif/esp-idf/examples/custom_bootloader/bootloader_override/build -P /home/dan/code/github.com/espressif/esp-idf/components/esptool_py/run_serial_tool.cmake
esptool esp32c3 -p /dev/ttyUSB0 -b 460800 --before=default_reset --after=hard_reset write_flash --flash_mode dio --flash_freq 80m --flash_size 2MB 0x0 bootloader/bootloader.bin 0x10000 main.bin 0x8000 partition_table/partition-table.bin
esptool.py v4.5.1
Serial port /dev/ttyUSB0
Connecting....
Chip is ESP32-C3 (revision v0.3)
Features: WiFi, BLE
Crystal is 40MHz
MAC: 58:cf:79:16:7d:a0
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 460800
Changed.
Configuring flash size...
Flash will be erased from 0x00000000 to 0x00005fff...
Flash will be erased from 0x00010000 to 0x00038fff...
Flash will be erased from 0x00008000 to 0x00008fff...
Compressed 20608 bytes to 12655...
Writing at 0x00000000... (100 %)
Wrote 20608 bytes (12655 compressed) at 0x00000000 in 0.7 seconds (effective 244.4 kbit/s)...
Hash of data verified.
Compressed 167424 bytes to 88511...
Writing at 0x00010000... (16 %)
Writing at 0x0001a51f... (33 %)
Writing at 0x0002104c... (50 %)
Writing at 0x00028442... (66 %)
Writing at 0x0002ec04... (83 %)
Writing at 0x00035e5c... (100 %)
Wrote 167424 bytes (88511 compressed) at 0x00010000 in 2.8 seconds (effective 477.3 kbit/s)...
Hash of data verified.
Compressed 3072 bytes to 103...
Writing at 0x00008000... (100 %)
Wrote 3072 bytes (103 compressed) at 0x00008000 in 0.1 seconds (effective 295.2 kbit/s)...
Hash of data verified.
Leaving...
Hard resetting via RTS pin...
Done
Much of the logic in this step can be found in the source for the write_flash command, but at a high level the steps are as follows:
- Load stub bootloader.
...
Uploading stub...
Running stub...
Stub running...
...
- Erase flash sections.
...
Configuring flash size...
Flash will be erased from 0x00000000 to 0x00005fff...
Flash will be erased from 0x00010000 to 0x00038fff...
Flash will be erased from 0x00008000 to 0x00008fff...
...
- Write second stage bootloader to flash.
...
Compressed 20608 bytes to 12655...
Writing at 0x00000000... (100 %)
Wrote 20608 bytes (12655 compressed) at 0x00000000 in 0.7 seconds (effective 244.4 kbit/s)...
Hash of data verified.
...
- Write application to flash.
...
Compressed 167424 bytes to 88511...
Writing at 0x00010000... (16 %)
Writing at 0x0001a51f... (33 %)
Writing at 0x0002104c... (50 %)
Writing at 0x00028442... (66 %)
Writing at 0x0002ec04... (83 %)
Writing at 0x00035e5c... (100 %)
Wrote 167424 bytes (88511 compressed) at 0x00010000 in 2.8 seconds (effective 477.3 kbit/s)...
Hash of data verified.
...
- Write partition table to flash.
...
Compressed 3072 bytes to 103...
Writing at 0x00008000... (100 %)
Wrote 3072 bytes (103 compressed) at 0x00008000 in 0.1 seconds (effective 295.2 kbit/s)...
Hash of data verified.
...
- Reset chip.
...
Leaving...
Hard resetting via RTS pin...
Done
In order to see what happens on boot now, we can once again connect via minicom and press the RST button.
$ minicom -D /dev/ttyUSB0
ESP-ROM:esp32c3-api1-20210207
Build:Feb 7 2021
rst:0x1 (POWERON),boot:0xc (SPI_FAST_FLASH_BOOT)
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fcd5820,len:0x1754
load:0x403cc710,len:0x970
load:0x403ce710,len:0x2f68
entry 0x403cc710
I (30) boot: ESP-IDF 4f0769d2ed 2nd stage bootloader
I (30) boot: compile time Apr 8 2023 18:51:23
I (30) boot: chip revision: v0.3
I (34) boot.esp32c3: SPI Speed : 80MHz
I (38) boot.esp32c3: SPI Mode : DIO
I (43) boot.esp32c3: SPI Flash Size : 2MB
I (48) boot: Enabling RNG early entropy source...
I (53) boot: Partition Table:
I (57) boot: ## Label Usage Type ST Offset Length
I (64) boot: 0 nvs WiFi data 01 02 00009000 00006000
I (71) boot: 1 phy_init RF data 01 01 0000f000 00001000
I (79) boot: 2 factory factory app 00 00 00010000 00100000
I (86) boot: End of partition table
[boot] Custom bootloader message defined in the KConfig file.
I (96) esp_image: segment 0: paddr=00010020 vaddr=3c020020 size=08480h ( 33920) map
I (110) esp_image: segment 1: paddr=000184a8 vaddr=3fc8aa00 size=01110h ( 4368) load
I (114) esp_image: segment 2: paddr=000195c0 vaddr=40380000 size=06a58h ( 27224) load
I (126) esp_image: segment 3: paddr=00020020 vaddr=42000020 size=15018h ( 86040) map
I (143) esp_image: segment 4: paddr=00035040 vaddr=40386a58 size=03d9ch ( 15772) load
I (149) boot: Loaded app from partition at offset 0x10000
I (149) boot: Disabling RNG early entropy source...
I (163) cpu_start: Pro cpu up.
I (172) cpu_start: Pro cpu start user code
I (172) cpu_start: cpu freq: 160000000 Hz
I (172) cpu_start: Application information:
I (175) cpu_start: Project name: main
I (180) cpu_start: App version: 4f0769d2ed
I (185) cpu_start: Compile time: Apr 8 2023 18:51:16
I (191) cpu_start: ELF file SHA256: 3916cd87115c6efe...
I (197) cpu_start: ESP-IDF: 4f0769d2ed
I (203) cpu_start: Min chip rev: v0.3
I (207) cpu_start: Max chip rev: v0.99
I (212) cpu_start: Chip rev: v0.3
I (217) heap_init: Initializing. RAM available for dynamic allocation:
I (224) heap_init: At 3FC8C940 len 0004FDD0 (319 KiB): DRAM
I (230) heap_init: At 3FCDC710 len 00002950 (10 KiB): STACK/DRAM
I (237) heap_init: At 50000020 len 00001FE0 (7 KiB): RTCRAM
I (244) spi_flash: detected chip: generic
I (248) spi_flash: flash io: dio
W (252) spi_flash: Detected size(4096k) larger than the size in the binary image header(2048k). Using the size in the binary image header.
I (265) sleep: Configure to isolate all GPIO pins in sleep state
I (272) sleep: Enable automatic switching of GPIO sleep configuration
I (279) app_start: Starting scheduler on CPU0
I (284) main_task: Started on CPU0
I (284) main_task: Calling app_main()
Application started!
I (294) main_task: Returned from app_main()
The second stage bootloader is loaded and executed successfully, as evidenced by the custom message.
[boot] Custom bootloader message defined in the KConfig file.
We can also see the message from the simple application that the bootloader jumps to.
Application started!
Looking Behind the Scenes
Now that we’ve run the example custom bootloader, let’s explore the binary to get a sense of what is happening behind the scenes. The entrypoint to the second stage bootloader is call_start_cpu0(void)
.
examples/custom_bootloader/bootloader_override/bootloader_components/main/bootloader_start.c
/*
* We arrive here after the ROM bootloader finished loading this second stage bootloader from flash.
* The hardware is mostly uninitialized, flash cache is down and the app CPU is in reset.
* We do have a stack, so we can do the initialization in C.
*/
void __attribute__((noreturn)) call_start_cpu0(void)
{
// 1. Hardware initialization
if (bootloader_init() != ESP_OK) {
bootloader_reset();
}
#ifdef CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP
// If this boot is a wake up from the deep sleep then go to the short way,
// try to load the application which worked before deep sleep.
// It skips a lot of checks due to it was done before (while first boot).
bootloader_utility_load_boot_image_from_deep_sleep();
// If it is not successful try to load an application as usual.
#endif
// 2. Select the number of boot partition
bootloader_state_t bs = {0};
int boot_index = select_partition_number(&bs);
if (boot_index == INVALID_INDEX) {
bootloader_reset();
}
// 2.1 Print a custom message!
esp_rom_printf("[%s] %s\n", TAG, CONFIG_EXAMPLE_BOOTLOADER_WELCOME_MESSAGE);
// 3. Load the app image for booting
bootloader_utility_load_boot_image(&bs, boot_index);
}
The provided comments are helpful to understand what is happening, and we’ll explore them in a moment, but how does the first stage bootloader know to jump here? Our custom bootloader is reusing the same linker script as the default second stage bootloader, which defines the entrypoint at the location of the call_start_cpu0
function.
components/bootloader/subproject/main/ld/esp32c3/bootloader.ld
/* Default entry point: */
ENTRY(call_start_cpu0);
It also defines a few memory regions for instruction memory (IRAM) and data memory (DRAM).
components/bootloader/subproject/main/ld/esp32c3/bootloader.ld
MEMORY
{
iram_seg (RWX) : org = bootloader_iram_seg_start, len = bootloader_iram_seg_len
iram_loader_seg (RWX) : org = bootloader_iram_loader_seg_start, len = bootloader_iram_loader_seg_len
dram_seg (RW) : org = bootloader_dram_seg_start, len = bootloader_dram_seg_len
}
The entrypoint is loaded at the beginning of th IRAM segment.
components/bootloader/subproject/main/ld/esp32c3/bootloader.ld
.iram.text :
{
. = ALIGN (16);
*(.entry.text)
*(.init.literal)
*(.init)
} > iram_seg
We can see this layout in effect by examining the ELF file we built for the bootloader image. If you successfully added all installed tools to your path, you should be able to use riscv32-esp-elf-objdump.
$ riscv32-esp-elf-objdump -h build/bootloader/bootloader.elf
build/bootloader/bootloader.elf: file format elf32-littleriscv
Sections:
Idx Name Size VMA LMA File off Algn
0 .iram_loader.text 00002f66 403ce710 403ce710 00003710 2**1
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .iram.text 00000000 403cc710 403cc710 00006676 2**0
CONTENTS
2 .dram0.bss 00000110 3fcd5710 3fcd5710 00000710 2**2
ALLOC
3 .dram0.data 00000004 3fcd5820 3fcd5820 00000820 2**2
CONTENTS, ALLOC, LOAD, DATA
4 .dram0.rodata 00001750 3fcd5824 3fcd5824 00000824 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .iram.text 0000096e 403cc710 403cc710 00002710 2**1
CONTENTS, ALLOC, LOAD, READONLY, CODE
6 .debug_info 000229fa 00000000 00000000 00006676 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
7 .debug_abbrev 000048b6 00000000 00000000 00029070 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
8 .debug_loc 000075d6 00000000 00000000 0002d926 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
9 .debug_aranges 00000808 00000000 00000000 00034efc 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
10 .debug_ranges 000015b8 00000000 00000000 00035704 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
11 .debug_line 00010e82 00000000 00000000 00036cbc 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
12 .debug_str 0000a1e0 00000000 00000000 00047b3e 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
13 .comment 0000002f 00000000 00000000 00051d1e 2**0
CONTENTS, READONLY
14 .riscv.attributes 0000003f 00000000 00000000 00051d4d 2**0
CONTENTS, READONLY
15 .debug_frame 000014bc 00000000 00000000 00051d8c 2**2
CONTENTS, READONLY, DEBUGGING, OCTETS
- NOTE: the -h flag indicates that we want just the section headers.
We see two .iram.text
sections, which look identical except for the Size and Algn (alignment). Because all of the .iram.text symbols are 16-bit aligned (ALIGN (16)), the 1-byte aligned section (2**0 or 2^0 = 1)
is empty, and all of the data is in the 2-byte aligned (2**0 or 2^1 = 2
) section. Both sections have the same virtual memory address (VMA), which makes sense given that the size of the first section is 0. We can jump to the .iram.text
section to see if our call_start_cpu0
function is indeed present.
$ riscv32-esp-elf-objdump -D -j .iram.text build/bootloader/bootloader.elf | head -46
build/bootloader/bootloader.elf: file format elf32-littleriscv
Disassembly of section .iram.text:
403cc710 <call_start_cpu0>:
403cc710: 7171 addi sp,sp,-176
403cc712: d706 sw ra,172(sp)
403cc714: d522 sw s0,168(sp)
403cc716: d326 sw s1,164(sp)
403cc718: 2895 jal 403cc78c <bootloader_init>
403cc71a: c119 beqz a0,403cc720 <call_start_cpu0+0x10>
403cc71c: 55a030ef jal ra,403cfc76 <bootloader_reset>
403cc720: 0a000613 li a2,160
403cc724: 4581 li a1,0
403cc726: 850a mv a0,sp
403cc728: ffc34097 auipc ra,0xffc34
403cc72c: c2c080e7 jalr -980(ra) # 40000354 <memset>
403cc730: 850a mv a0,sp
403cc732: 19a030ef jal ra,403cf8cc <bootloader_utility_load_partition_table>
403cc736: 3fcd64b7 lui s1,0x3fcd6
403cc73a: ed19 bnez a0,403cc758 <call_start_cpu0+0x48>
403cc73c: 688020ef jal ra,403cedc4 <esp_log_early_timestamp>
403cc740: 85aa mv a1,a0
403cc742: 3fcd6537 lui a0,0x3fcd6
403cc746: 86c48613 addi a2,s1,-1940 # 3fcd586c <_data_end+0x48>
403cc74a: 87450513 addi a0,a0,-1932 # 3fcd5874 <_data_end+0x50>
403cc74e: ffc34097 auipc ra,0xffc34
403cc752: 8f2080e7 jalr -1806(ra) # 40000040 <esp_rom_printf>
403cc756: b7d9 j 403cc71c <call_start_cpu0+0xc>
403cc758: 850a mv a0,sp
403cc75a: 3a0030ef jal ra,403cfafa <bootloader_utility_get_selected_boot_partition>
403cc75e: f9d00793 li a5,-99
403cc762: 842a mv s0,a0
403cc764: faf50ce3 beq a0,a5,403cc71c <call_start_cpu0+0xc>
403cc768: 3fcd6637 lui a2,0x3fcd6
403cc76c: 3fcd6537 lui a0,0x3fcd6
403cc770: 86c48593 addi a1,s1,-1940
403cc774: 8a860613 addi a2,a2,-1880 # 3fcd58a8 <_data_end+0x84>
403cc778: 8e050513 addi a0,a0,-1824 # 3fcd58e0 <_data_end+0xbc>
403cc77c: ffc34097 auipc ra,0xffc34
403cc780: 8c4080e7 jalr -1852(ra) # 40000040 <esp_rom_printf>
403cc784: 85a2 mv a1,s0
403cc786: 850a mv a0,sp
403cc788: 50a030ef jal ra,403cfc92 <bootloader_utility_load_boot_image>
- NOTE: the -D flag indicates that we want to disassemble all section contents. The -j .iram.text indicates that we only want the contents of the .iram.text section.
[https://danielmangum.com/posts/risc-v-bytes-caller-callee-registers/]
Here we see the familiar function prologue of growing our stack (addi sp,sp,-176) and storing our callee-saved registers (s0, s1) on it, before progressing through the various calls to bootloader utility functions. The bootloader_init() implementation varies per SoC, but the high-level steps are fairly similar. The esp32c3 implementation is shown below.
components/bootloader_support/src/esp32c3/bootloader_esp32c3.c
esp_err_t bootloader_init(void)
{
esp_err_t ret = ESP_OK;
bootloader_hardware_init();
bootloader_ana_reset_config();
bootloader_super_wdt_auto_feed();
// In RAM_APP, memory will be initialized in `call_start_cpu0`
#if !CONFIG_APP_BUILD_TYPE_RAM
// protect memory region
bootloader_init_mem();
/* check that static RAM is after the stack */
assert(&_bss_start <= &_bss_end);
assert(&_data_start <= &_data_end);
// clear bss section
bootloader_clear_bss_section();
#endif // !CONFIG_APP_BUILD_TYPE_RAM
// init eFuse virtual mode (read eFuses to RAM)
#ifdef CONFIG_EFUSE_VIRTUAL
ESP_LOGW(TAG, "eFuse virtual mode is enabled. If Secure boot or Flash encryption is enabled then it does not provide any security. FOR TESTING ONLY!");
#ifndef CONFIG_EFUSE_VIRTUAL_KEEP_IN_FLASH
esp_efuse_init_virtual_mode_in_ram();
#endif
#endif
// config clock
bootloader_clock_configure();
// initialize console, from now on, we can use esp_log
bootloader_console_init();
/* print 2nd bootloader banner */
bootloader_print_banner();
#if !CONFIG_APP_BUILD_TYPE_PURE_RAM_APP
//init cache hal
cache_hal_init();
//init mmu
mmu_hal_init();
// update flash ID
bootloader_flash_update_id();
// Check and run XMC startup flow
if ((ret = bootloader_flash_xmc_startup()) != ESP_OK) {
ESP_LOGE(TAG, "failed when running XMC startup flow, reboot!");
return ret;
}
#if !CONFIG_APP_BUILD_TYPE_RAM
// read bootloader header
if ((ret = bootloader_read_bootloader_header()) != ESP_OK) {
return ret;
}
// read chip revision and check if it's compatible to bootloader
if ((ret = bootloader_check_bootloader_validity()) != ESP_OK) {
return ret;
}
#endif //#if !CONFIG_APP_BUILD_TYPE_RAM
// initialize spi flash
if ((ret = bootloader_init_spi_flash()) != ESP_OK) {
return ret;
}
#endif // !CONFIG_APP_BUILD_TYPE_PURE_RAM_APP
// check whether a WDT reset happend
bootloader_check_wdt_reset();
// config WDT
bootloader_config_wdt();
// enable RNG early entropy source
bootloader_enable_random();
return ret;
}
If all initialization is successful (i.e. bootloader_init()
returns 0), we jump over the call to bootloader_reset()
with beqz and continue to loading the application.
403cc71a: c119 beqz a0,403cc720 <call_start_cpu0+0x10>
403cc71c: 55a030ef jal ra,403cfc76 <bootloader_reset>
403cc720: 0a000613 li a2,160
[https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32c3/api-guides/partition-tables.html]
In addition to the bootloader and application binaries, we also saw that a partition table was constructed and loaded into flash (it was the last item and had size of only 3072 bytes). The partition table informs the bootloader where various data is located in flash. We saw a visual representation of its contents in the bootloader logs when we reset the CPU.
I (53) boot: Partition Table:
I (57) boot: ## Label Usage Type ST Offset Length
I (64) boot: 0 nvs WiFi data 01 02 00009000 00006000
I (71) boot: 1 phy_init RF data 01 01 0000f000 00001000
I (79) boot: 2 factory factory app 00 00 00010000 00100000
I (86) boot: End of partition table
In this case, we are loading the factory partition, which is where our application was written to flash (0x00010000
). There are a number of steps in this process, but the sequence maps to the following function calls.
- void bootloader_utility_load_boot_image(const bootloader_state_t *bs, int start_index): determines where the desired image exists and, if it cannot be found, goes through a series of fallback options.
- static void load_image(const esp_image_metadata_t *image_data): copies loaded segments to RAM and sets up caches for mapped segments.
- static void unpack_load_app(const esp_image_metadata_t *data): configures mappings for MMU.
The final step is to actually start the application, which is accomplished by defining an entry symbol at the entry_addr, then calling it.
components/bootloader_support/src/bootloader_utility.c
static void set_cache_and_start_app(
uint32_t drom_addr,
uint32_t drom_load_addr,
uint32_t drom_size,
uint32_t irom_addr,
uint32_t irom_load_addr,
uint32_t irom_size,
uint32_t entry_addr)
{
...
ESP_LOGD(TAG, "start: 0x%08"PRIx32, entry_addr);
bootloader_atexit();
typedef void (*entry_t)(void) __attribute__((noreturn));
entry_t entry = ((entry_t) entry_addr);
// TODO: we have used quite a bit of stack at this point.
// use "movsp" instruction to reset stack back to where ROM stack starts.
(*entry)();
}
Concluding Thoughts
[https://github.com/hasheddan/]
[https://github.com/hasheddan/HashedDan.github.io/tree/master/posts/risc-v-bytes-exploring-custom-esp32-bootloader]
Bootloaders are a great place to look when you want to understand how software and hardware communicate. While you may never need to modify the bootloader yourself, knowledge of how it works is useful for conceptualizing how changes made at higher levels utlimately translate to the machine.
As always, these posts are meant to serve as a useful resource for folks who are interested in learning more about RISC-V and low-level software in general. If I can do a better job of reaching that goal, or you have any questions or comments, please feel free to send me a message @hasheddan on Twitter or @hasheddan@types.pl on Mastodon!
联系方式
如果对本文有疑问或者提出建议可评论区留言或者发送邮件到2557877116@qq.com.