Skip to content

Commit 5558114

Browse files
committed
feat: add USB CDC support, ReadFlash protocol, and ESP32-S3 post-connect
- chunk SLIP writes to 64 bytes for USB CDC endpoint compatibility - use 1KB RAM upload blocks for USB connections - detect USB-OTG and USB-JTAG/Serial interfaces on ESP32-S3 - disable RTC WDT and enable SWD auto-feed for USB-JTAG/Serial - add ReadFlash protocol with SLIP-framed blocks and cumulative ACKs - add SLIP leftover buffer for multi-frame USB transfers - add port re-open recovery for TinyUSB CDC re-enumeration
1 parent 483acff commit 5558114

8 files changed

Lines changed: 498 additions & 43 deletions

File tree

pkg/espflasher/chip.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ type chipDef struct {
117117

118118
// FlashSizes maps size strings to header byte values.
119119
FlashSizes map[string]byte
120+
121+
// PostConnect is called after chip detection to perform chip-specific
122+
// initialization (e.g. USB interface detection, watchdog disable).
123+
// May set Flasher fields like usesUSB.
124+
PostConnect func(f *Flasher) error
120125
}
121126

122127
// chipDetectMagicRegAddr is the register address that has a different

pkg/espflasher/flasher.go

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,20 +105,23 @@ type connection interface {
105105
changeBaud(newBaud, oldBaud uint32) error
106106
eraseFlash() error
107107
eraseRegion(offset, size uint32) error
108+
readFlash(offset, size uint32) ([]byte, error)
108109
flushInput()
109110
isStub() bool
111+
setUSB(v bool)
110112
setSupportsEncryptedFlash(v bool)
111113
loadStub(s *stub) error
112114
}
113115

114116
// Flasher manages the connection to an ESP device and provides
115117
// high-level flash operations.
116118
type Flasher struct {
117-
port serial.Port
118-
conn connection
119-
chip *chipDef
120-
opts *FlasherOptions
121-
portStr string
119+
port serial.Port
120+
conn connection
121+
chip *chipDef
122+
opts *FlasherOptions
123+
portStr string
124+
usesUSB bool
122125
}
123126

124127
// New creates a new Flasher connected to the given serial port.
@@ -162,7 +165,7 @@ func New(portName string, opts *FlasherOptions) (*Flasher, error) {
162165

163166
// Connect to the bootloader
164167
if err := f.connect(); err != nil {
165-
port.Close() //nolint:errcheck
168+
f.port.Close() //nolint:errcheck
166169
return nil, err
167170
}
168171

@@ -174,6 +177,31 @@ func (f *Flasher) Close() error {
174177
return f.port.Close()
175178
}
176179

180+
// reopenPort closes and reopens the serial port after a USB device
181+
// re-enumeration. TinyUSB CDC devices may briefly disappear during reset.
182+
func (f *Flasher) reopenPort() error {
183+
f.port.Close() //nolint:errcheck
184+
185+
var lastErr error
186+
deadline := time.Now().Add(3 * time.Second)
187+
for time.Now().Before(deadline) {
188+
time.Sleep(500 * time.Millisecond)
189+
port, err := serial.Open(f.portStr, &serial.Mode{
190+
BaudRate: f.opts.BaudRate,
191+
Parity: serial.NoParity,
192+
DataBits: 8,
193+
StopBits: serial.OneStopBit,
194+
})
195+
if err == nil {
196+
f.port = port
197+
f.conn = newConn(port)
198+
return nil
199+
}
200+
lastErr = err
201+
}
202+
return fmt.Errorf("reopen port %s: %w", f.portStr, lastErr)
203+
}
204+
177205
// ChipType returns the detected chip type.
178206
func (f *Flasher) ChipType() ChipType {
179207
if f.chip != nil {
@@ -224,6 +252,11 @@ func (f *Flasher) connect() error {
224252
}
225253
time.Sleep(50 * time.Millisecond)
226254
}
255+
256+
// Sync failed — try reopening port (USB CDC may have re-enumerated)
257+
if err := f.reopenPort(); err != nil {
258+
continue // port reopen failed, try next attempt
259+
}
227260
}
228261

229262
return &SyncError{Attempts: attempts}
@@ -248,9 +281,21 @@ synced:
248281

249282
f.logf("Detected chip: %s", f.chip.Name)
250283

284+
// Run chip-specific post-connect initialization.
285+
if f.chip.PostConnect != nil {
286+
if err := f.chip.PostConnect(f); err != nil {
287+
f.logf("Warning: post-connect: %v", err)
288+
}
289+
}
290+
251291
// Propagate chip capabilities to the connection layer.
252292
f.conn.setSupportsEncryptedFlash(f.chip.SupportsEncryptedFlash)
253293

294+
// Propagate USB flag to connection layer for block size optimization.
295+
if f.usesUSB {
296+
f.conn.setUSB(true)
297+
}
298+
254299
// Upload the stub loader to enable advanced features (erase, compression, etc.).
255300
if s, ok := stubFor(f.chip.ChipType); ok {
256301
f.logf("Loading stub loader...")
@@ -679,6 +724,20 @@ func (f *Flasher) WriteRegister(addr, value uint32) error {
679724
return f.conn.writeReg(addr, value, 0xFFFFFFFF, 0)
680725
}
681726

727+
// ReadFlash reads data from flash memory.
728+
// Requires the stub loader to be running.
729+
func (f *Flasher) ReadFlash(offset, size uint32) ([]byte, error) {
730+
if !f.conn.isStub() {
731+
return nil, &UnsupportedCommandError{Command: "read flash (requires stub)"}
732+
}
733+
734+
if err := f.attachFlash(); err != nil {
735+
return nil, err
736+
}
737+
738+
return f.conn.readFlash(offset, size)
739+
}
740+
682741
// Reset performs a hard reset of the device, causing it to run user code.
683742
func (f *Flasher) Reset() {
684743
if f.conn.isStub() {
@@ -693,7 +752,7 @@ func (f *Flasher) Reset() {
693752
// CMD_FLASH_BEGIN after a compressed download may interfere with
694753
// the flash controller state at offset 0. esptool also just does
695754
// a hard reset without any flash commands for the ROM path.
696-
hardReset(f.port, false)
755+
hardReset(f.port, f.usesUSB)
697756
f.logf("Device reset.")
698757
}
699758

pkg/espflasher/flasher_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,3 +411,18 @@ func TestFlashSizeFromJEDECMatchesChipSizes(t *testing.T) {
411411
}
412412
}
413413
}
414+
415+
func TestReadFlashRequiresStub(t *testing.T) {
416+
mock := &mockConnection{}
417+
mock.stubMode = false // ROM mode
418+
f := &Flasher{conn: mock, chip: chipDefs[ChipESP32]}
419+
_, err := f.ReadFlash(0, 1024)
420+
if err == nil {
421+
t.Fatal("expected error when stub is not running")
422+
}
423+
if ue, ok := err.(*UnsupportedCommandError); !ok {
424+
t.Errorf("expected UnsupportedCommandError, got %T: %v", err, err)
425+
} else if ue.Command != "read flash (requires stub)" {
426+
t.Errorf("unexpected error message: %s", ue.Command)
427+
}
428+
}

pkg/espflasher/protocol.go

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ const (
5959
// flashSectorSize is the minimum flash erase unit.
6060
flashSectorSize uint32 = 0x1000 // 4KB
6161

62+
// readFlashBlockSize is the block size for read flash operations.
63+
readFlashBlockSize uint32 = 0x1000 // 4KB
64+
6265
// espImageMagic is the first byte of a valid ESP firmware image.
6366
espImageMagic byte = 0xE9
6467

@@ -79,7 +82,8 @@ const (
7982
type conn struct {
8083
port serial.Port
8184
reader *slipReader
82-
stub bool
85+
stub bool
86+
usesUSB bool // set for USB-OTG and USB-JTAG/Serial connections
8387
// supportsEncryptedFlash indicates the ROM supports the 5th parameter
8488
// (encrypted flag) in flash_begin/flash_defl_begin commands.
8589
// Set based on chip type after detection.
@@ -91,6 +95,11 @@ func (c *conn) isStub() bool {
9195
return c.stub
9296
}
9397

98+
// setUSB sets whether the connection uses USB-OTG or USB-JTAG endpoints.
99+
func (c *conn) setUSB(v bool) {
100+
c.usesUSB = v
101+
}
102+
94103
// setSupportsEncryptedFlash sets whether the ROM supports encrypted flash commands.
95104
func (c *conn) setSupportsEncryptedFlash(v bool) {
96105
c.supportsEncryptedFlash = v
@@ -125,8 +134,20 @@ func (c *conn) sendCommand(opcode byte, data []byte, chk uint32) error {
125134
copy(pkt[8:], data)
126135

127136
frame := slipEncode(pkt)
128-
_, err := c.port.Write(frame)
129-
return err
137+
// USB CDC endpoints have limited buffer sizes. Writing large SLIP frames
138+
// in one shot can overflow the endpoint buffer and cause data loss.
139+
// Chunk writes to 64 bytes (standard USB Full Speed bulk endpoint size).
140+
const maxChunk = 64
141+
for off := 0; off < len(frame); off += maxChunk {
142+
end := off + maxChunk
143+
if end > len(frame) {
144+
end = len(frame)
145+
}
146+
if _, err := c.port.Write(frame[off:end]); err != nil {
147+
return err
148+
}
149+
}
150+
return nil
130151
}
131152

132153
// commandResponse represents a parsed response from the ESP device.
@@ -541,6 +562,51 @@ func (c *conn) eraseRegion(offset, size uint32) error {
541562
return err
542563
}
543564

565+
// readFlash reads data from flash memory (stub-only).
566+
func (c *conn) readFlash(offset, size uint32) ([]byte, error) {
567+
data := make([]byte, 16)
568+
binary.LittleEndian.PutUint32(data[0:4], offset)
569+
binary.LittleEndian.PutUint32(data[4:8], size)
570+
binary.LittleEndian.PutUint32(data[8:12], readFlashBlockSize)
571+
binary.LittleEndian.PutUint32(data[12:16], 64) // max_inflight (stub clamps to 1)
572+
573+
if _, err := c.checkCommand("read flash", cmdReadFlash, data, 0, defaultTimeout, 0); err != nil {
574+
return nil, err
575+
}
576+
577+
blockTimeout := defaultTimeout + time.Duration(readFlashBlockSize/256)*100*time.Millisecond
578+
numBlocks := (size + readFlashBlockSize - 1) / readFlashBlockSize
579+
result := make([]byte, 0, size)
580+
581+
for i := uint32(0); i < numBlocks; i++ {
582+
// Read SLIP-framed data block
583+
block, err := c.reader.ReadFrame(blockTimeout)
584+
if err != nil {
585+
return nil, fmt.Errorf("read flash block %d/%d: %w", i+1, numBlocks, err)
586+
}
587+
result = append(result, block...)
588+
589+
// Send ACK: cumulative bytes received (SLIP-framed)
590+
ack := make([]byte, 4)
591+
binary.LittleEndian.PutUint32(ack, uint32(len(result)))
592+
ackFrame := slipEncode(ack)
593+
if _, err := c.port.Write(ackFrame); err != nil {
594+
return nil, fmt.Errorf("read flash ACK %d/%d: %w", i+1, numBlocks, err)
595+
}
596+
}
597+
598+
// Read final 16-byte MD5 digest (SLIP-framed)
599+
_, err := c.reader.ReadFrame(defaultTimeout)
600+
if err != nil {
601+
return nil, fmt.Errorf("read flash MD5: %w", err)
602+
}
603+
604+
if uint32(len(result)) > size {
605+
result = result[:size]
606+
}
607+
return result, nil
608+
}
609+
544610
// flashWriteSize returns the appropriate block size based on loader type.
545611
func (c *conn) flashWriteSize() uint32 {
546612
if c.stub {
@@ -601,17 +667,24 @@ func (c *conn) loadStub(s *stub) error {
601667

602668
// uploadToRAM writes a binary segment to the device's RAM via mem_begin/mem_data.
603669
func (c *conn) uploadToRAM(data []byte, addr uint32) error {
670+
// USB CDC endpoints have limited buffer sizes. Use 1KB blocks for
671+
// USB connections instead of the default 6KB to avoid timeout.
672+
blockSize := espRAMBlock
673+
if c.usesUSB {
674+
blockSize = 0x400 // 1KB
675+
}
676+
604677
dataLen := uint32(len(data))
605-
numBlocks := (dataLen + espRAMBlock - 1) / espRAMBlock
678+
numBlocks := (dataLen + blockSize - 1) / blockSize
606679

607-
if err := c.memBegin(dataLen, numBlocks, espRAMBlock, addr); err != nil {
680+
if err := c.memBegin(dataLen, numBlocks, blockSize, addr); err != nil {
608681
return err
609682
}
610683

611684
seq := uint32(0)
612685
offset := uint32(0)
613686
for offset < dataLen {
614-
end := offset + espRAMBlock
687+
end := offset + blockSize
615688
if end > dataLen {
616689
end = dataLen
617690
}

0 commit comments

Comments
 (0)