Skip to content

Commit 3e5c1ae

Browse files
jgangemiclaude
andcommitted
feat: add USB CDC support and ReadFlash protocol
- chunk SLIP writes to 64 bytes for USB CDC endpoint compatibility - use 1KB RAM upload blocks for USB connections - 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 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b62650b commit 3e5c1ae

File tree

6 files changed

+333
-38
lines changed

6 files changed

+333
-38
lines changed

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: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,10 @@ 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
}
@@ -119,6 +121,7 @@ type Flasher struct {
119121
chip *chipDef
120122
opts *FlasherOptions
121123
portStr string
124+
usesUSB bool
122125
secInfo []byte // cached security info from ROM (GET_SECURITY_INFO opcode 0x14)
123126
}
124127

@@ -163,7 +166,7 @@ func New(portName string, opts *FlasherOptions) (*Flasher, error) {
163166

164167
// Connect to the bootloader
165168
if err := f.connect(); err != nil {
166-
port.Close() //nolint:errcheck
169+
f.port.Close() //nolint:errcheck
167170
return nil, err
168171
}
169172

@@ -175,6 +178,31 @@ func (f *Flasher) Close() error {
175178
return f.port.Close()
176179
}
177180

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

239272
return &SyncError{Attempts: attempts}
@@ -258,9 +291,21 @@ synced:
258291

259292
f.logf("Detected chip: %s", f.chip.Name)
260293

294+
// Run chip-specific post-connect initialization.
295+
if f.chip.PostConnect != nil {
296+
if err := f.chip.PostConnect(f); err != nil {
297+
f.logf("Warning: post-connect: %v", err)
298+
}
299+
}
300+
261301
// Propagate chip capabilities to the connection layer.
262302
f.conn.setSupportsEncryptedFlash(f.chip.SupportsEncryptedFlash)
263303

304+
// Propagate USB flag to connection layer for block size optimization.
305+
if f.usesUSB {
306+
f.conn.setUSB(true)
307+
}
308+
264309
// Upload the stub loader to enable advanced features (erase, compression, etc.).
265310
if s, ok := stubFor(f.chip.ChipType); ok {
266311
f.logf("Loading stub loader...")
@@ -717,6 +762,20 @@ func (f *Flasher) GetMD5(offset, size uint32) (string, error) {
717762
return hex.EncodeToString(result), nil
718763
}
719764

765+
// ReadFlash reads data from flash memory.
766+
// Requires the stub loader to be running.
767+
func (f *Flasher) ReadFlash(offset, size uint32) ([]byte, error) {
768+
if !f.conn.isStub() {
769+
return nil, &UnsupportedCommandError{Command: "read flash (requires stub)"}
770+
}
771+
772+
if err := f.attachFlash(); err != nil {
773+
return nil, err
774+
}
775+
776+
return f.conn.readFlash(offset, size)
777+
}
778+
720779
// Reset performs a hard reset of the device, causing it to run user code.
721780
func (f *Flasher) Reset() {
722781
if f.conn.isStub() {
@@ -731,7 +790,7 @@ func (f *Flasher) Reset() {
731790
// CMD_FLASH_BEGIN after a compressed download may interfere with
732791
// the flash controller state at offset 0. esptool also just does
733792
// a hard reset without any flash commands for the ROM path.
734-
hardReset(f.port, false)
793+
hardReset(f.port, f.usesUSB)
735794
f.logf("Device reset.")
736795
}
737796

pkg/espflasher/flasher_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,21 @@ func TestGetMD5RequiresStub(t *testing.T) {
449449
}
450450
}
451451

452+
func TestReadFlashRequiresStub(t *testing.T) {
453+
mock := &mockConnection{}
454+
mock.stubMode = false // ROM mode
455+
f := &Flasher{conn: mock, chip: chipDefs[ChipESP32]}
456+
_, err := f.ReadFlash(0, 1024)
457+
if err == nil {
458+
t.Fatal("expected error when stub is not running")
459+
}
460+
if ue, ok := err.(*UnsupportedCommandError); !ok {
461+
t.Errorf("expected UnsupportedCommandError, got %T: %v", err, err)
462+
} else if ue.Command != "read flash (requires stub)" {
463+
t.Errorf("unexpected error message: %s", ue.Command)
464+
}
465+
}
466+
452467
func TestGetSecurityInfo(t *testing.T) {
453468
secInfo := make([]byte, 20)
454469
binary.LittleEndian.PutUint32(secInfo[0:4], 0x05)

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.
@@ -542,6 +563,51 @@ func (c *conn) eraseRegion(offset, size uint32) error {
542563
return err
543564
}
544565

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

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

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

612685
seq := uint32(0)
613686
offset := uint32(0)
614687
for offset < dataLen {
615-
end := offset + espRAMBlock
688+
end := offset + blockSize
616689
if end > dataLen {
617690
end = dataLen
618691
}

0 commit comments

Comments
 (0)