添加esp32-c3镜像格式文档
294
internal/app/appflash/appflash.go
Normal 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] 为 status(0 == 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))
|
||||
}
|
||||
67
internal/native/docs/esp32-c3/firmware-image-format.md
Normal 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`.
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
| 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.
|
||||
|
||||
|
After Width: | Height: | Size: 20 KiB |
BIN
internal/native/docs/esp32-c3/images/command_packet_format.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
BIN
internal/native/docs/esp32-c3/images/firmware_image_format.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
BIN
internal/native/docs/esp32-c3/images/response_packet_format.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
9
internal/native/docs/esp32-c3/readme.md
Normal 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
|
||||
|
||||
116
internal/native/docs/esp32-c3/serial-protocol.md
Normal 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.
|
||||
|
||||

|
||||
|
||||
| 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:
|
||||
|
||||

|
||||
|
||||
| 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 don’t 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
|
||||