完善esp32flash子命令

This commit is contained in:
chai2010
2025-11-21 06:15:54 +08:00
parent 8c2b94030e
commit 5239c8b29e
9 changed files with 426 additions and 155 deletions

View File

@@ -52,11 +52,6 @@ var CmdESP32Build = &cli.Command{
os.Exit(1)
}
if c.NArg() == 0 {
fmt.Fprintln(os.Stderr, "no input file")
os.Exit(1)
}
parser.DebugMode = c.Bool("debug")
infile := c.Args().First()

View File

@@ -15,11 +15,7 @@ var CmdESP32Dump = &cli.Command{
Hidden: true,
Name: "esp32dump",
Usage: "dump esp32 image file",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "hello",
},
},
Flags: []cli.Flag{},
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
fmt.Fprintln(os.Stderr, "no input file")

View File

@@ -0,0 +1,63 @@
// Copyright (C) 2025 武汉凹语言科技有限公司
// SPDX-License-Identifier: AGPL-3.0-or-later
package appesp32flash
import (
"fmt"
"os"
"wa-lang.org/wa/internal/3rdparty/cli"
"wa-lang.org/wa/internal/native/espflash"
)
var CmdESP32Flash = &cli.Command{
Hidden: true,
Name: "esp32flash",
Usage: "upload esp32 image file to board",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "port",
Aliases: []string{"p"},
Usage: "set uart port",
},
&cli.IntFlag{
Name: "baudrate",
Aliases: []string{"b"},
Usage: "set uart baudrate",
Value: 115200,
},
&cli.IntFlag{
Name: "addr",
Aliases: []string{"a"},
Usage: "set flash address",
},
},
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
fmt.Fprintln(os.Stderr, "no input file")
os.Exit(1)
}
portName := c.String("port")
baudRate := c.Int("baudrate")
flashAddr := c.Int("addr")
filePath := c.Args().First()
client, err := espflash.Open(portName, baudRate)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer client.Close()
err = client.FlashFile(uint32(flashAddr), filePath)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println("Done")
return nil
},
}

View File

@@ -0,0 +1,257 @@
// Copyright (C) 2025 武汉凹语言科技有限公司
// SPDX-License-Identifier: AGPL-3.0-or-later
package espflash
import (
"crypto/md5"
"encoding/binary"
"errors"
"fmt"
"os"
"wa-lang.org/wa/internal/3rdparty/serial"
"wa-lang.org/wa/internal/3rdparty/slip"
)
type Client struct {
port *serial.Port
r *slip.Reader
w *slip.Writer
}
func Open(name string, baudrate int) (*Client, error) {
p := new(Client)
port, err := serial.OpenPort(
&serial.Config{
Name: name,
Baud: baudrate,
},
)
if err != nil {
return nil, err
}
p.port = port
p.r = slip.NewReader(port)
p.w = slip.NewWriter(port)
return p, nil
}
func (p *Client) Close() error {
return p.port.Close()
}
func (c *Client) SendCommand(cmd CmdOpcode, payload []byte, checksum uint32) ([]byte, error) {
if err := c.sendRequest(cmd, payload, checksum); err != nil {
return nil, err
}
return c.readResponse()
}
func (c *Client) sendFrame(data []byte) error {
return c.w.WritePacket(data)
}
func (c *Client) readFrame() (p []byte, isPrefix bool, err error) {
return c.r.ReadPacket()
}
func (c *Client) sendRequest(cmd CmdOpcode, payload []byte, checksum uint32) error {
// Assemble request frame
frame := make([]byte, 0, 8+len(payload))
frame = append(frame, 0x00) // direction: 0x00 = host -> target
frame = append(frame, byte(cmd))
// payload length
lenb := make([]byte, 2)
binary.LittleEndian.PutUint16(lenb, uint16(len(payload)))
frame = append(frame, lenb...)
// payload
frame = append(frame, payload...)
// checksum
cksum := make([]byte, 4)
binary.LittleEndian.PutUint32(cksum, checksum)
frame = append(frame, cksum...)
return c.sendFrame(frame)
}
// readResponse reads a response frame:
//
// Status(1) + Length(2) + Payload
func (c *Client) readResponse() ([]byte, error) {
resp, _, err := c.readFrame()
if err != nil {
return nil, err
}
if len(resp) < 3 {
return nil, errors.New("invalid response length")
}
status := resp[0]
if status != 0 {
return nil, ErrorCode(status)
}
respLen := binary.LittleEndian.Uint16(resp[1:3])
if int(respLen)+3 != len(resp) {
return nil, errors.New("invalid response length field")
}
if status != 0 {
return nil, fmt.Errorf("rom loader error: 0x%02x", status)
}
// ok
return resp[3:], nil
}
func (f *Client) CmdSync() error {
payload := make([]byte, 36)
payload[0] = 0x07
payload[1] = 0x07
payload[2] = 0x12
payload[3] = 0x20
for i := 4; i < 36; i++ {
payload[i] = 0x55
}
_, err := f.SendCommand(CmdSync, payload, 0)
return err
}
func (f *Client) CmdEraseFlash() error {
_, err := f.SendCommand(CmdEraseFlash, nil, 0)
return err
}
func (f *Client) CmdBeginWrite(addr, totalSize, blockSize uint32) error {
if blockSize == 0 {
return errors.New("blockSize cannot be 0")
}
packets := (totalSize + blockSize - 1) / blockSize
eraseSize := packets * blockSize
payload := make([]byte, 5*4)
binary.LittleEndian.PutUint32(payload[0:], eraseSize)
binary.LittleEndian.PutUint32(payload[4:], packets)
binary.LittleEndian.PutUint32(payload[8:], blockSize)
binary.LittleEndian.PutUint32(payload[12:], addr)
binary.LittleEndian.PutUint32(payload[16:], 0)
_, err := f.SendCommand(CmdFlashBegin, payload, 0)
return err
}
func (f *Client) CmdWriteBlock(blockIndex uint32, data []byte) error {
payload := make([]byte, 16+len(data))
binary.LittleEndian.PutUint32(payload[0:], uint32(len(data)))
binary.LittleEndian.PutUint32(payload[4:], blockIndex)
binary.LittleEndian.PutUint32(payload[8:], 0)
binary.LittleEndian.PutUint32(payload[12:], 0)
copy(payload[16:], data)
// TODO: 计算 checksum, 按 ROM 协议
var sum uint32
for _, b := range data {
sum += uint32(b)
}
_, err := f.SendCommand(CmdFlashData, payload, sum)
return err
}
func (f *Client) CmdEndWrite(reboot bool) error {
payload := make([]byte, 4)
if reboot {
binary.LittleEndian.PutUint32(payload, 0)
} else {
binary.LittleEndian.PutUint32(payload, 1)
}
_, err := f.SendCommand(CmdFlashEnd, payload, 0)
return err
}
func (f *Client) CmdFlashBinary(addr uint32, bin []byte) error {
const blockSize = 0x400 // 1024 bytes
if err := f.CmdBeginWrite(addr, uint32(len(bin)), blockSize); err != nil {
return err
}
var blockIndex uint32
for offset := 0; offset < len(bin); offset += blockSize {
end := offset + blockSize
if end > len(bin) {
end = len(bin)
}
chunk := bin[offset:end]
if err := f.CmdWriteBlock(blockIndex, chunk); err != nil {
return err
}
blockIndex++
}
if err := f.CmdEndWrite(false); err != nil {
return err
}
return nil
}
func (c *Client) FlashFile(addr uint32, filePath string) error {
bin, err := os.ReadFile(filePath)
if err != nil {
return err
}
if err := c.CmdSync(); err != nil {
fmt.Println("Sync failed, retrying...")
if err := c.CmdSync(); err != nil {
return fmt.Errorf("sync failed: %v", err)
}
}
if err := c.CmdFlashBinary(addr, bin); err != nil {
return fmt.Errorf("flash failed: %v", err)
}
ok, err := c.verifyFlashMD5(addr, bin)
if err != nil {
return err
}
if !ok {
return errors.New("flash verify failed: MD5 mismatch")
}
return nil
}
func (c *Client) verifyFlashMD5(addr uint32, bin []byte) (bool, error) {
const CmdSPIFlashMD5 = 0x13
payload := make([]byte, 16)
binary.LittleEndian.PutUint32(payload[0:], addr)
binary.LittleEndian.PutUint32(payload[4:], uint32(len(bin)))
binary.LittleEndian.PutUint32(payload[8:], 0)
binary.LittleEndian.PutUint32(payload[12:], 0)
resp, err := c.SendCommand(CmdSPIFlashMD5, payload, 0)
if err != nil {
return false, fmt.Errorf("SPI_FLASH_MD5 command failed: %v", err)
}
if len(resp) < 16 {
return false, errors.New("MD5 response too short")
}
deviceMD5 := resp[:16]
localMD5 := md5.Sum(bin)
return string(deviceMD5) == string(localMD5[:]), nil
}

View File

@@ -0,0 +1,32 @@
// Copyright (C) 2025 武汉凹语言科技有限公司
// SPDX-License-Identifier: AGPL-3.0-or-later
package espflash
type CmdOpcode byte
const (
CmdFlashBegin CmdOpcode = 0x02
CmdFlashData CmdOpcode = 0x03
CmdFlashEnd CmdOpcode = 0x04
CmdMemBegin CmdOpcode = 0x05
CmdMemEnd CmdOpcode = 0x06
CmdMemData CmdOpcode = 0x07
CmdSync CmdOpcode = 0x08
CmdWriteReg CmdOpcode = 0x09
CmdReadReg CmdOpcode = 0x0A
CmdSPISetParams CmdOpcode = 0x0B
CmdSPIAttach CmdOpcode = 0x0D
CmdChangeBaudrate CmdOpcode = 0x0F
CmdFlashDeflBegin CmdOpcode = 0x10
CmdFlashDeflData CmdOpcode = 0x11
CmdFlashDeflEnd CmdOpcode = 0x12
CmdSPIFlashMD5 CmdOpcode = 0x13
CmdGetSecurityInfo CmdOpcode = 0x14
// Stub only
CmdEraseFlash CmdOpcode = 0xd0
CmdEraseRegion CmdOpcode = 0xd1
CmdReadFlash CmdOpcode = 0xd2
CmdRunUserCode CmdOpcode = 0xd3
)

View File

@@ -0,0 +1,71 @@
// Copyright (C) 2025 武汉凹语言科技有限公司
// SPDX-License-Identifier: AGPL-3.0-or-later
package espflash
import "fmt"
type ErrorCode uint8
const (
ErrROMUndefined ErrorCode = 0x00
ErrROMInvalidParam ErrorCode = 0x01
ErrROMMallocFailed ErrorCode = 0x02
ErrROMSendFailed ErrorCode = 0x03
ErrROMRecvFailed ErrorCode = 0x04
ErrROMInvalidFormat ErrorCode = 0x05
ErrROMResultError ErrorCode = 0x06
ErrROMChecksumError ErrorCode = 0x07
ErrROMFlashWriteError ErrorCode = 0x08
ErrROMFlashReadError ErrorCode = 0x09
ErrROMFlashReadLenError ErrorCode = 0x0a
ErrROMDeflateFailed ErrorCode = 0x0b
ErrROMDeflateAdlerError ErrorCode = 0x0c
ErrROMDeflateParamError ErrorCode = 0x0d
ErrROMInvalidRAMSize ErrorCode = 0x0e
ErrROMInvalidRAMAddr ErrorCode = 0x0f
// Extended errors (0x64+)
ErrROMInvalidParameter ErrorCode = 0x64
ErrROMInvalidFmt ErrorCode = 0x65
ErrROMDescriptionTooLong ErrorCode = 0x66
ErrROMBadEncoding ErrorCode = 0x67
ErrROMInsufficientStorage ErrorCode = 0x69
)
// description map
var romErrorDescriptions = map[ErrorCode]string{
ErrROMUndefined: "Undefined errors",
ErrROMInvalidParam: "The input parameter is invalid",
ErrROMMallocFailed: "Failed to malloc memory from system",
ErrROMSendFailed: "Failed to send out message",
ErrROMRecvFailed: "Failed to receive message",
ErrROMInvalidFormat: "The format of the received message is invalid",
ErrROMResultError: "Message is ok, but the running result is wrong",
ErrROMChecksumError: "Checksum error",
ErrROMFlashWriteError: "Flash write error",
ErrROMFlashReadError: "Flash read error",
ErrROMFlashReadLenError: "Flash read length error",
ErrROMDeflateFailed: "Deflate failed error",
ErrROMDeflateAdlerError: "Deflate Adler32 error",
ErrROMDeflateParamError: "Deflate parameter error",
ErrROMInvalidRAMSize: "Invalid RAM binary size",
ErrROMInvalidRAMAddr: "Invalid RAM binary address",
ErrROMInvalidParameter: "Invalid parameter",
ErrROMInvalidFmt: "Invalid format",
ErrROMDescriptionTooLong: "Description too long",
ErrROMBadEncoding: "Bad encoding description",
ErrROMInsufficientStorage: "Insufficient storage",
}
func (e ErrorCode) String() string {
if s, ok := romErrorDescriptions[e]; ok {
return s
}
return fmt.Sprintf("Unknown ROM error 0x%02X", uint8(e))
}
func (e ErrorCode) Error() string {
return e.String()
}

View File

@@ -1,110 +0,0 @@
package main
import (
"fmt"
"io"
"log"
"os"
"time"
"wa-lang.org/wa/internal/3rdparty/serial"
)
// 烧写配置
const (
portName = "/dev/ttyUSB0" // 根据您的系统修改为正确的串口名称 (e.g., COM3 on Windows)
baudRate = 115200
dataFile = "your_firmware.bin" // 假设这是你的汇编器生成的固件
flashAddr = 0x42000000 // ESP32-C3 Flash的典型起始地址之一
)
func main() {
// 1. 打开串口
c := &serial.Config{Name: portName, Baud: baudRate}
port, err := serial.OpenPort(c)
if err != nil {
log.Fatalf("无法打开串口 %s: %v", portName, err)
}
defer port.Close()
log.Printf("成功打开串口 %s", portName)
// 2. 模拟 ROM Bootloader 握手 (SYNC)
// 实际操作中,你需要发送一个复杂的命令序列,这里仅模拟发送
fmt.Println("--- 模拟 ROM Bootloader 握手 ---")
// 假设的同步命令(实际请查阅文档)
syncCmd := []byte{0x07, 0x07, 0x12, 0x20}
n, err := port.Write(syncCmd)
if err != nil {
log.Fatal(err)
}
log.Printf("发送了 %d 字节的同步命令。", n)
// 等待设备响应 (实际需要解析响应,这里只是等待)
time.Sleep(100 * time.Millisecond)
// 3. 模拟发送固件
fmt.Println("--- 模拟发送固件数据 ---")
firmware, err := os.ReadFile(dataFile)
if err != nil {
log.Fatalf("无法读取固件文件 %s: %v", dataFile, err)
}
// 假设的写入 Flash 命令头 (实际会包含地址、长度、校验和等)
// 在实际烧写中,会是 (Command Header + Data Segment Header + Firmware Chunk)
dataChunkSize := 256 // 假设每次发送256字节
for i := 0; i < len(firmware); i += dataChunkSize {
end := i + dataChunkSize
if end > len(firmware) {
end = len(firmware)
}
chunk := firmware[i:end]
// 实际烧写时,需要在发送数据块前发送一个带有目标地址和长度的命令包
// 模拟发送数据块
n, err := port.Write(chunk)
if err != nil {
log.Fatal(err)
}
log.Printf("发送数据块: 地址 0x%X, 长度 %d 字节", flashAddr+i, n)
// 实际烧写中,每发送一个块都需要等待设备的 ACK 确认
time.Sleep(1 * time.Millisecond)
}
// 4. 模拟运行命令 (RUN)
fmt.Println("--- 模拟 RUN 命令 ---")
// 实际操作中,你需要发送一个 RUN 命令,指定程序入口地址
// 5. 切换到串口监视模式
fmt.Println("--- 切换到串口监视模式,等待设备输出 ---")
// 启动一个 goroutine 来持续读取串口数据
go func() {
buf := make([]byte, 128)
for {
n, err := port.Read(buf)
if err != nil {
if err != io.EOF {
fmt.Printf("\n串口读取错误: %v\n", err)
}
return
}
if n > 0 {
fmt.Printf("%s", buf[:n])
}
}
}()
// 保持主程序运行,直到用户按下 Enter
fmt.Println("烧写和监视器已启动。按下 Enter 键退出...")
fmt.Scanln()
}

View File

@@ -1,35 +0,0 @@
package main
// ESP32-C3 固件映像头 (Image Header)
// 结构体字段顺序和大小必须与硬件文档严格匹配
type ImageHeader struct {
Magic uint32 // 魔术字:必须是 0xE9
SegmentCount byte // 段的数量 (对于单段程序为 1)
SpiMode byte // SPI Flash 模式 (通常为 0x02)
SpiSpeed byte // SPI Flash 速度 (通常为 0x01)
SpiSize byte // SPI Flash 大小 (通常为 0x20)
EntryAddr uint32 // 程序入口点地址 (例如: 0x42000000)
Reserved [16]byte // 保留字段
Checksum byte // 映像头和所有段头的校验和(通常放在最后计算)
}
// 段头 (Segment Header)
type SegmentHeader struct {
LoadAddr uint32 // 该段数据将被加载到的 RAM/IRAM 地址
DataLen uint32 // 该段数据的字节长度
}
const (
ESP_IMAGE_MAGIC = 0xE9 // ESP32-C3 固件映像魔术字
// 假设的程序入口点地址 (Flash 地址)
// ROM Bootloader 会将它作为入口点执行
DEFAULT_ENTRY_ADDR = 0x42000000
)
// 假设这是您的 Go 汇编器生成的原始机器指令
var rawMachineCode = []byte{
// 假设这是 RISC-V 指令的机器码序列
0x13, 0x05, 0x00, 0x00, // li a0, 0
0x67, 0x80, 0x00, 0x00, // ret
// 实际代码会更长,包括 UART 操作
}