Building and Injecting¶
A module is a standard relocatable ELF object compiled from plain C. C3PO automates the whole pipeline from the Modules view; this page documents what happens under the hood and how to install the toolchains.
The pipeline¶
flowchart LR
SRC["module .c"] --> CC["compile<br/>(arch toolchain)"]
CC --> ELF["relocatable ELF (.o)"]
ELF --> SIGN["sign<br/>HKDF + HMAC-SHA256"]
SIGN --> BLOB["[32B HMAC][ELF]"]
BLOB --> XFER["mod_chunk x N + mod_load"]
XFER --> DEV["verify -> relocate -> run"]
1. Compile¶
C3PO selects the toolchain from the target chip and compiles the entry .c to a relocatable object:
| Chip | Toolchain |
|---|---|
| esp32 | xtensa-esp32-elf-gcc |
| esp32s2 | xtensa-esp32s2-elf-gcc |
| esp32s3 | xtensa-esp32s3-elf-gcc |
| esp32c3 / esp32c6 / esp32h2 | riscv32-esp-elf-gcc |
The compiler flags differ by architecture and are mandatory:
# Xtensa (ESP32, S2, S3)
xtensa-esp32-elf-gcc -c -Os -fno-common \
-mlongcalls -mtext-section-literals \
-I /path/to/ESPM/include \
module.c -o module.o
# RISC-V (C3, C6, H2)
riscv32-esp-elf-gcc -c -Os -fno-common \
-march=rv32imc -mabi=ilp32 \
-I /path/to/ESPM/include \
module.c -o module.o
Xtensa: -mlongcalls is required
Without -mlongcalls -mtext-section-literals, the compiler emits R_XTENSA_SLOT0_OP
relocations the loader cannot apply, and the module will fail to load.
C3PO also passes -DCONFIG_DEVICE_ID="..." so a module can know its device id, and adds the ESPM include/ directory (the standalone espilon-test/ESPM/include, or the firmware copy as a fallback). Toolchains are found on PATH or under ~/.espressif/tools.
2. Sign¶
The ELF is signed with a key derived from the device's 32-byte master_key:
sign_key = HKDF-SHA256(master_key, len=32, salt=None, info="espm-sign")
blob = HMAC-SHA256(sign_key, elf) || elf # 32-byte tag prepended
Because the key is the device key, each module is signed per device. The firmware verifies the tag before relocation; a blob signed for one device will not load on another.
3. Inject¶
The signed blob is transferred to the device with the firmware built-in commands:
mod_chunk- append a chunk to the staging buffer (sent N times for a large module).mod_load- finalize: verify the HMAC, validate and relocate the ELF, load it into IRAM (or PSRAM on S3), and start the task.
Other built-ins manage loaded modules: mod_list, mod_stop, mod_purge, and mod_unpersist.
Relocation types applied at load¶
The loader patches the object for the target architecture:
- Xtensa:
R_XTENSA_32,R_XTENSA_ASM_EXPAND(no-op),R_XTENSA_SLOT0_OP(avoided via-mlongcalls). - RISC-V:
R_RISCV_32,R_RISCV_HI20,R_RISCV_LO12_I/LO12_S,R_RISCV_CALL/CALL_PLT,R_RISCV_BRANCH,R_RISCV_RVC_BRANCH/RVC_JUMP,R_RISCV_RELAX(hint).
Installing the cross-compilers¶
The ESP-IDF toolchains provide the gcc cross-compilers. For RISC-V targets you can also install the Espressif package directly:
Xtensa targets use the xtensa-esp32[-s2|-s3]-elf-gcc compilers from an ESP-IDF install.
Size limits¶
A module must fit within ESPM_MAX_CODE_SIZE (64 KB .text) and ESPM_MAX_DATA_SIZE (32 KB .data + .bss). The loader rejects anything larger.