添加esp32-c3镜像格式文档

This commit is contained in:
chai2010
2025-11-17 22:33:36 +08:00
parent d4bf8c2d36
commit a6b19db900
12 changed files with 488 additions and 0 deletions

View File

@@ -0,0 +1,294 @@
// Copyright (C) 2024 武汉凹语言科技有限公司
// SPDX-License-Identifier: AGPL-3.0-or-later
package appflash
import (
"bufio"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"time"
"wa-lang.org/wa/internal/3rdparty/cli"
"wa-lang.org/wa/internal/3rdparty/serial"
)
var CmdFlash = &cli.Command{
Hidden: true,
Name: "flash",
Usage: "flash a bare-metal firmware image to an ESP32-C3 device.",
ArgsUsage: "[firmware.bin]",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "port, p",
Usage: "Serial port device to use for flashing (e.g., /dev/ttyUSB0 or COM3)",
Value: "/dev/ttyUSB0",
Aliases: []string{"p"},
},
&cli.IntFlag{
Name: "baud, b",
Usage: "Baud rate for flashing communication",
Value: 115200,
Aliases: []string{"b"},
},
&cli.StringFlag{
Name: "addr, a",
Usage: "Flash memory address to start writing the firmware (e.g., 0x42000000)",
Value: "0x42000000",
Aliases: []string{"a"},
},
},
Action: func(c *cli.Context) error {
if false {
cmdMain() // TODO
}
return nil
},
}
const (
ESP_SYNC = 0x08
ESP_FLASH_BEGIN = 0x02
ESP_FLASH_DATA = 0x03
ESP_FLASH_END = 0x04
)
func cmdMain() {
port := openPort("/dev/ttyUSB0", 115200)
// step 1: handshake
if err := espSync(port); err != nil {
panic(err)
}
// step 2: load firmware
data, _ := os.ReadFile("firmware.bin")
// step 3: flash
flash(port, data, 0x0) // 地址 0x0
}
func openPort(name string, baud int) io.ReadWriteCloser {
cfg := &serial.Config{
Name: name,
Baud: baud,
ReadTimeout: time.Millisecond * 500,
}
p, err := serial.OpenPort(cfg)
if err != nil {
panic(err)
}
return p
}
func slipEncode(data []byte) []byte {
out := []byte{0xC0}
for _, b := range data {
switch b {
case 0xC0:
out = append(out, 0xDB, 0xDC)
case 0xDB:
out = append(out, 0xDB, 0xDD)
default:
out = append(out, b)
}
}
out = append(out, 0xC0)
return out
}
func espSync(port io.ReadWriteCloser) error {
// SYNC payload固定
payload := []byte{
0x07, 0x07, 0x12, 0x20,
}
for i := 0; i < 32; i++ {
payload = append(payload, 0x55)
}
// 组包
packet := buildPacket(ESP_SYNC, payload)
port.Write(packet)
// 等待设备回应
buf := make([]byte, 100)
_, err := port.Read(buf)
return err
}
func buildPacket(cmd byte, data []byte) []byte {
header := make([]byte, 8)
header[0] = cmd
header[1] = byte(len(data))
header[2] = byte(len(data) >> 8)
header[3] = byte(len(data) >> 16)
header[4] = 0 // checksum; XOR
header[5] = 0
header[6] = 0
header[7] = 0
// 简单 XOR 校验
chk := byte(0)
for _, b := range data {
chk ^= b
}
header[4] = chk
return slipEncode(append(header, data...))
}
func flash(port io.ReadWriteCloser, fw []byte, offset int) {
blockSize := 0x400
numBlocks := (len(fw) + blockSize - 1) / blockSize
// FLASH_BEGIN
beginData := make([]byte, 16)
binary.LittleEndian.PutUint32(beginData[0:], uint32(len(fw)))
binary.LittleEndian.PutUint32(beginData[4:], uint32(numBlocks))
binary.LittleEndian.PutUint32(beginData[8:], uint32(blockSize))
binary.LittleEndian.PutUint32(beginData[12:], uint32(offset))
p := buildPacket(ESP_FLASH_BEGIN, beginData)
port.Write(p)
if err := waitAckOK(port); err != nil {
panic(err)
}
// FLASH_DATA
for seq := 0; seq < numBlocks; seq++ {
start := seq * blockSize
end := start + blockSize
if end > len(fw) {
end = len(fw)
}
buf := fw[start:end]
data := make([]byte, 16)
binary.LittleEndian.PutUint32(data[0:], uint32(len(buf)))
binary.LittleEndian.PutUint32(data[4:], uint32(seq))
binary.LittleEndian.PutUint32(data[8:], uint32(0)) // flash offset; unused
binary.LittleEndian.PutUint32(data[12:], uint32(0))
p := buildPacket(ESP_FLASH_DATA, append(data, buf...))
port.Write(p)
waitAck(port)
}
// FLASH_END
endData := make([]byte, 4)
binary.LittleEndian.PutUint32(endData, 1) // reboot = 1
p = buildPacket(ESP_FLASH_END, endData)
port.Write(p)
if err := waitAckOK(port); err != nil {
panic(err)
}
}
// ErrTimeout 表示等待响应超时
var ErrTimeout = errors.New("timeout waiting for response")
// waitAck 从串口读取一个 SLIP 包并返回解码后的 payload。超时返回 ErrTimeout。
// 注意:上层需要根据协议检查 payload 内容(比如 header 或状态码)。
func waitAck(port io.ReadWriteCloser) ([]byte, error) {
// 使用短超时避免永久阻塞;如果你在高波特率下需要更短或更长,可调整
return readSLIPPacket(port, 3*time.Second)
}
// readSLIPPacket 读取一个以 0xC0 开始并以 0xC0 结束的 SLIP 包处理转义字节0xDB
// 返回的是 SLIP 解码后的内部 payload不含外层 0xC0
func readSLIPPacket(port io.ReadWriteCloser, timeout time.Duration) ([]byte, error) {
r := bufio.NewReader(port)
deadline := time.After(timeout)
// 等待起始 0xC0
for {
select {
case <-deadline:
return nil, ErrTimeout
default:
}
b, err := r.ReadByte()
if err != nil {
// 如果串口设置了 ReadTimeout会因为超时返回错误把它包装成超时
if err == io.EOF {
// EOF from underlying stream, treat as timeout-like
return nil, ErrTimeout
}
return nil, err
}
if b == 0xC0 {
break
}
// otherwise keep skipping until start marker
}
// 读取直到下一个 0xC0处理转义
out := make([]byte, 0, 256)
for {
select {
case <-deadline:
return nil, ErrTimeout
default:
}
b, err := r.ReadByte()
if err != nil {
if err == io.EOF {
return nil, ErrTimeout
}
return nil, err
}
if b == 0xC0 {
// end of SLIP packet
break
}
if b == 0xDB {
// escape sequence: next byte should be 0xDC or 0xDD
nb, err := r.ReadByte()
if err != nil {
if err == io.EOF {
return nil, ErrTimeout
}
return nil, err
}
if nb == 0xDC {
out = append(out, 0xC0)
} else if nb == 0xDD {
out = append(out, 0xDB)
} else {
// 非标准转义:把两字节都放回(尽量恢复)
out = append(out, nb)
}
} else {
out = append(out, b)
}
// 简单保护:避免无穷增长
if len(out) > 10*1024*1024 {
return nil, fmt.Errorf("frame too large")
}
}
return out, nil
}
func waitAckOK(port io.ReadWriteCloser) error {
payload, err := waitAck(port)
if err != nil {
return err
}
// 下面是示例性的“成功判定”。按你需要替换为真实的协议字段判断。
if len(payload) >= 4 {
// 假设 header[0] 为 status0 == OK
if payload[0] == 0x00 {
return nil
}
return fmt.Errorf("device returned error status: 0x%02x", payload[0])
}
return fmt.Errorf("unexpected response length: %d", len(payload))
}

View File

@@ -0,0 +1,67 @@
# Firmware Image Format
https://docs.espressif.com/projects/esptool/en/latest/esp32c3/advanced-topics/firmware-image-format.html
This is technical documentation for the firmware image format used by the ROM bootloader. These are the images created by `esptool elf2image`.
![](./images/firmware_image_format.png)
The firmware file consists of a header, an extended header, a variable number of data segments and a footer. Multi-byte fields are little-endian.
## File Header
![](./images/firmware_image_header_format.png)
The image header is 8 bytes long:
| Byte | Description |
| ---- | ----------- |
| 0 | Magic number (always `0xE9`) |
| 1 | Number of segments |
| 2 | SPI Flash Mode (0 = QIO, 1 = QOUT, 2 = DIO, 3 = DOUT) |
| 3 | High four bits - Flash size (0 = 1MB, 1 = 2MB, 2 = 4MB, 3 = 8MB, 4 = 16MB) |
| 3 | Low four bits - Flash frequency (0 = 40MHz, 1 = 26MHz, 2 = 20MHz, 0xf = 80MHz) |
| 4-7 | Entry point address |
`esptool` overrides the 2nd and 3rd (counted from 0) bytes according to the SPI flash info provided through the command line options (see :ref:`flash-modes`).
These bytes are only overridden if this is a bootloader image (an image written to a correct bootloader offset of {IDF_TARGET_BOOTLOADER_OFFSET}).
In this case, the appended SHA256 digest, which is a cryptographic hash used to verify the integrity of the image, is also updated to reflect the header changes.
Generating images without SHA256 digest can be achieved by running `esptool elf2image` with the `--dont-append-digest` argument.
## Extended File Header
![](./images/firmware_image_ext_header_format.png)
| Byte | Description |
| ---- | ----------- |
| 0 | WP pin when SPI pins set via eFuse (read by ROM bootloader)
| 1-3 | Drive settings for the SPI flash pins (read by ROM bootloader)
| 4-5 | Chip ID (which ESP device is this image for)
| 6 | Minimal chip revision supported by the image (deprecated, use the following field)
| 7-8 | Minimal chip revision supported by the image (in format: major * 100 + minor)
| 9-10 | Maximal chip revision supported by the image (in format: major * 100 + minor)
| 11-14 | Reserved bytes in additional header space, currently unused
| 15 | Hash appended (If 1, SHA256 digest is appended after the checksum)
## Segment
| Byte | Description |
| ---- | ----------- |
| 0-3 | Memory offset |
| 4-7 | Segment size
| 8…n | Data |
## Footer
The file is padded with zeros until its size is one byte less than a multiple of 16 bytes. A last byte (thus making the file size a multiple of 16) is the checksum of the data of all segments. The checksum is defined as the xor-sum of all bytes and the byte `0xEF`.
If `hash appended` in the extended file header is `0x01`, a SHA256 digest “simple hash” (of the entire image) is appended after the checksum. This digest is separate to secure boot and only used for detecting corruption. The SPI flash info cannot be changed during flashing if hash is appended after the image.
If secure boot is enabled, a signature is also appended (and the simple hash is included in the signed data). This image signature is [Secure Boot V1](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/security/secure-boot-v1.html#image-signing-algorithm) and [Secure Boot V2](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/security/secure-boot-v2.html#signature-block-format) specific.
## Analyzing a Binary Image
To analyze a binary image and get a complete summary of its headers and segments, use the [image-info](https://docs.espressif.com/projects/esptool/en/latest/esp32c3/esptool/basic-commands.html#image-info) command.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,9 @@
# ESP32-C3
- Serial Protocol
- https://docs.espressif.com/projects/esptool/en/latest/esp32c3/advanced-topics/serial-protocol.html
- Firmware Image Format
- https://docs.espressif.com/projects/esptool/en/latest/esp32c3/advanced-topics/firmware-image-format.html
- ESP32-C3 Datasheet
- https://documentation.espressif.com/esp32-c3_datasheet_en.pdf

View File

@@ -0,0 +1,116 @@
# Serial Protocol
https://docs.espressif.com/projects/esptool/en/latest/esp32c3/advanced-topics/serial-protocol.html
This is technical documentation for the serial protocol used by the UART bootloader in the {IDF_TARGET_NAME} ROM and the esptool [stub loader](https://docs.espressif.com/projects/esptool/en/latest/esp32c3/esptool/flasher-stub.html#stub) program.
The UART bootloader runs on chip reset if certain strapping pins are set. See [Entering-the-bootloader](https://docs.espressif.com/projects/esptool/en/latest/esp32c3/esptool/entering-bootloader.html#entering-the-bootloader) for details of this process.
By default, esptool uploads a stub "software loader" to the IRAM of the chip. The stub loader then replaces the ROM loader for all future interactions. This standardizes much of the behavior. Pass ``--no-stub`` to esptool in order to disable the stub loader. See [Flasher stub](https://docs.espressif.com/projects/esptool/en/latest/esp32c3/esptool/flasher-stub.html#stub) for more information.
> Note: There are differences in the serial protocol between ESP chips! To switch to documentation for a different chip, choose the desired target from the dropdown menu in the upper left corner.
## Packet Description
The host computer sends a SLIP encoded command request to the ESP chip. The ESP chip responds to the request with a SLIP encoded response packet, including status information and any data as a payload.
## Low Level Protocol
The bootloader protocol uses [SLIP](https://en.wikipedia.org/wiki/Serial_Line_Internet_Protocol) packet framing for data transmissions in both directions.
Each SLIP packet begins and ends with `0xC0`. Within the packet, all occurrences of `0xC0` and `0xDB` are replaced with `0xDB 0xDC` and `0xDB 0xDD`, respectively. The replacing is to be done **after** the checksum and lengths are calculated, so the packet length may be longer than the `size` field below.
## Command Packet
Each command is a SLIP packet initiated by the host and results in a response packet. Inside the packet, the packet consists of a header and a variable-length body. All multi-byte fields are little-endian.
![](./images/command_packet_format.png)
| Byte | Name | Comment |
| ---- | ---- | ------- |
| 0 | Direction | Always `0x00` for requests
| 1 | Command | Command identifier (see [Commands](https://docs.espressif.com/projects/esptool/en/latest/esp32c3/advanced-topics/serial-protocol.html#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](https://docs.espressif.com/projects/esptool/en/latest/esp32c3/advanced-topics/serial-protocol.html#checksum)).
| 8..n | Data | Variable length data payload (0-65535 bytes, as indicated by Size parameter). Usage depends on specific command.
## Response Packet
Each received command will result in a response SLIP packet sent from the ESP chip to the host. Contents of the response packet is:
![](./images/response_packet_format.png)
| 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.
## Status Bytes
The final bytes of the Data payload indicate command status:
For stub loader the final two bytes indicate status (most commands return at least a two byte Data payload):
| Byte | Name | Comment |
| ---- | ---- | ------- |
| Size-2 | Status | Status flag, success (0) or failure (1)
| Size-1 | Error | If Status is 1, this indicates the type of error.
For ESP32-C3 ROM (only, not the stub loader) the final four bytes are used, but only the first two bytes contain status information:
| Byte | Name | Comment |
| ---- | ---- | ------- |
| Size-4 | Status | Status flag, success (0) or failure (1)
| Size-3 | Error | If Status 1, this indicates the type of error.
| Size-2 | Reserved
| Size-1 | Reserved
## ROM Loader Errors
The ROM loader sends the following error values
| Value | Meaning |
| ----- | ------- |
| `0x00` | “Undefined errors”
| `0x01` | “The input parameter is invalid”
| `0x02` | “Failed to malloc memory from system”
| `0x03` | “Failed to send out message”
| `0x04` | “Failed to receive message”
| `0x05` | “The format of the received message is invalid”
| `0x06` | “Message is ok, but the running result is wrong”
| `0x07` | “Checksum error”
| `0x08` | “Flash write error” - after writing a block of data to flash, the ROM loader reads the value back and the 8-bit CRC is compared to the data read from flash. If they dont match, this error is returned.
| `0x09` | “Flash read error” - SPI read failed
| `0x0a` | “Flash read length error” - SPI read request length is wrong
| `0x0b` | “Deflate failed error” (compressed uploads only)
| `0x0c` | “Deflate Adler32 error”
| `0x0d` | “Deflate parameter error”
| `0x0e` | “Invalid RAM binary size”
| `0x0f` | “Invalid RAM binary address”
| `0x64` | “Invalid parameter”
| `0x65` | “Invalid format”
| `0x66` | “Description too long”
| `0x67` | “Bad encoding description”
| `0x69` | “Insufficient storage”
## Stub Loader Status & Error
If the stub loader is used:
- The status response is always 2 bytes regardless of chip type.
- Stub loader error codes are entirely different to the ROM loader codes. They all take the form `0xC*`, or `0xFF` for "unimplemented command". ([Full list here](https://github.com/espressif/esptool/blob/master/flasher_stub/include/stub_flasher.h#L95)).
After sending a command, the host should continue to read response packets until one is received where the Command field matches the request's Command field, or a timeout is exceeded.
## Commands
### Supported by Stub Loader and ROM Loader
TODO