Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ for _, e := range entries {
| `usb-jtag` | `ResetUSBJTAG` | Reset sequence for boards with a native USB-JTAG/Serial interface (ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2). Use this when connected via `/dev/ttyACM0` (Linux) or `cu.usbmodem*` (macOS). |
| `no-reset` | `ResetNoReset` | Skip hardware reset entirely. Useful when the chip is already in bootloader mode or reset is handled externally. |

If the port is a program's USB-OTG CDC device with the Espressif VID (not the 303a:1001 USB-JTAG/Serial unit), every mode except `no-reset` first sends a 1200 baud touch. A program that supports it, such as TinyGo with `-tags=usbotg` on the ESP32-S3, restarts into download mode on USB-JTAG/Serial, and espflasher continues on that port.

## API Overview

### Creating a Flasher
Expand Down
3 changes: 3 additions & 0 deletions pkg/espflasher/flasher.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,9 @@ func (f *Flasher) connect() error {
// UART bridge resets don't enter download mode on USB-Serial/JTAG.
// Reference: esptool loader.py _construct_reset_strategy_sequence().
resetMode := f.opts.ResetMode
if resetMode != ResetNoReset && f.touchUSBOTG() {
resetMode = ResetUSBJTAG
}
if resetMode == ResetDefault && f.usbInterfaceFromPort() == usbInterfaceSerialJTAG {
f.logf("USB-JTAG/Serial port detected, using USB-JTAG reset")
resetMode = ResetUSBJTAG
Expand Down
23 changes: 23 additions & 0 deletions pkg/espflasher/usbjtag_enumerate.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,26 @@ func enumeratePortVIDPID(name string) (vid, pid string, ok bool) {
}
return "", "", false
}

// findUSBJTAGPort returns the USB-Serial/JTAG port, preferring the given
// name. ok is false if there is no such port or more than one to choose from.
var findUSBJTAGPort = func(prefer string) (name string, ok bool) {
ports, err := enumerator.GetDetailedPortsList()
if err != nil {
return "", false
}
var found []string
for _, p := range ports {
if !p.IsUSB || strings.ToUpper(p.VID) != espressifUSBVID || strings.ToUpper(p.PID) != usbSerialJTAGPID {
continue
}
if samePortName(p.Name, prefer) {
return p.Name, true
}
found = append(found, p.Name)
}
if len(found) != 1 {
return "", false
}
return found[0], true
}
2 changes: 2 additions & 0 deletions pkg/espflasher/usbjtag_nocgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ package espflasher
// portVIDPID can't enumerate USB devices on macOS without cgo; callers fall
// back to reading UARTDEV_BUF_NO out of ROM.
var portVIDPID = func(name string) (vid, pid string, ok bool) { return "", "", false }

var findUSBJTAGPort = func(prefer string) (name string, ok bool) { return "", false }
50 changes: 50 additions & 0 deletions pkg/espflasher/usbotg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package espflasher

import (
"strconv"
"time"

"go.bug.st/serial"
)

// touchUSBOTG sends a 1200 baud touch to a program that runs USB-OTG CDC
// with the Espressif VID, so it restarts into download mode on USB-Serial/JTAG.
// It reports whether the flasher now uses that USB-Serial/JTAG port.
func (f *Flasher) touchUSBOTG() bool {
vid, pid, ok := portVIDPID(f.portStr)
if !ok || vid != espressifUSBVID || pid == usbSerialJTAGPID {
return false
}
// The ROM's own USB-OTG port uses the image chip ID as PID, as matched
// by esptool uses_usb_otg(). It is already in download mode.
if n, err := strconv.ParseUint(pid, 16, 16); err != nil || n < 0x100 {
return false
}
f.logf("USB-OTG port detected, sending 1200 baud touch")

// Ioctls on a cdc_acm fd can block once the device disconnects.
port := f.port
done := make(chan struct{})
go func() {
port.SetMode(&serial.Mode{BaudRate: 1200}) //nolint:errcheck
port.SetDTR(false) //nolint:errcheck
close(done)
}()
select {
case <-done:
case <-time.After(500 * time.Millisecond):
}
port.Close() //nolint:errcheck

deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
time.Sleep(250 * time.Millisecond)
if name, ok := findUSBJTAGPort(f.portStr); ok {
f.portStr = name
return f.reopenPort() == nil
}
}
f.logf("No USB-JTAG/Serial port after 1200 baud touch")
f.reopenPort() //nolint:errcheck
return false
}
79 changes: 79 additions & 0 deletions pkg/espflasher/usbotg_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package espflasher

import (
"testing"

"github.com/stretchr/testify/assert"
"go.bug.st/serial"
)

type touchPort struct {
mockPort
baud int
dtrOff bool
}

func (p *touchPort) SetMode(mode *serial.Mode) error {
p.baud = mode.BaudRate
return nil
}

func (p *touchPort) SetDTR(dtr bool) error {
p.dtrOff = !dtr
return nil
}

func stubFindUSBJTAGPort(t *testing.T, name string, ok bool) {
t.Helper()
prev := findUSBJTAGPort
findUSBJTAGPort = func(string) (string, bool) { return name, ok }
t.Cleanup(func() { findUSBJTAGPort = prev })
}

func TestTouchUSBOTGSkipsOtherPorts(t *testing.T) {
tests := []struct {
name string
vid, pid string
}{
{"usb-serial-jtag", "303A", "1001"},
{"rom usb-otg s3", "303A", "0009"},
{"usb-uart bridge", "10C4", "EA60"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
stubPortVIDPID(t, tt.vid, tt.pid, true)
p := &touchPort{}
f := &Flasher{port: p, opts: &FlasherOptions{}, portStr: "/dev/ttyACM0"}
assert.False(t, f.touchUSBOTG())
assert.Equal(t, 0, p.baud)
assert.Equal(t, 0, p.closeCalls)
})
}
}

func TestTouchUSBOTGSwitchesToUSBJTAGPort(t *testing.T) {
stubPortVIDPID(t, "303A", "4001", true)
stubFindUSBJTAGPort(t, "/dev/ttyACM1", true)
app := &touchPort{}
rom := &mockPort{}
var opened string
opts := &FlasherOptions{
Logger: &StdoutLogger{W: nopWriter{}},
SerialOpener: func(name string, mode *serial.Mode) (serial.Port, error) {
opened = name
return rom, nil
},
}
f := &Flasher{port: app, conn: newConn(app), opts: opts, portStr: "/dev/ttyACM0"}

assert.True(t, f.touchUSBOTG())
assert.Equal(t, 1200, app.baud)
assert.True(t, app.dtrOff)
assert.Equal(t, "/dev/ttyACM1", f.portStr)
assert.Equal(t, "/dev/ttyACM1", opened)
assert.Same(t, serial.Port(rom), f.port)
}

type nopWriter struct{}

func (nopWriter) Write(p []byte) (int, error) { return len(p), nil }
Loading