Skip to content
Open
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
10 changes: 10 additions & 0 deletions pkg/espflasher/chip.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,16 @@ type chipDef struct {
// initialization (e.g. USB interface detection, watchdog disable).
// May set Flasher fields like usesUSB.
PostConnect func(f *Flasher) error

// HardResetOTG is an optional chip-specific hook tried first by
// Flasher.Reset(). It distinguishes chips with a native USB-OTG
// peripheral (e.g. ESP32-S2), where the DTR/RTS latch used by
// hardResetUSB is a no-op, from chips with a USB-Serial-JTAG bridge
// (S3, C3, C6, H2, C5), which still use that latch. It returns true
// if it performed the reset; false to fall back to the standard
// usesUSB/hardReset path. Nil for chips without a native-OTG reset
// mechanism.
HardResetOTG func(f *Flasher) bool
}

// chipDetectMagicRegAddr is the register address that has a different
Expand Down
14 changes: 11 additions & 3 deletions pkg/espflasher/flasher.go
Original file line number Diff line number Diff line change
Expand Up @@ -895,14 +895,22 @@ func (f *Flasher) Reset() {
if f.conn.isStub() {
// Tell the stub to cleanly exit flash mode and reboot.
// flashEnd(true) triggers a software reboot inside the stub.
// For ROM bootloaders, skip flash_begin/flash_end — sending
// CMD_FLASH_BEGIN after a compressed download may interfere with
// the flash controller state at offset 0.
f.conn.flashBegin(0, 0, false) //nolint:errcheck
f.conn.flashEnd(true) //nolint:errcheck
time.Sleep(50 * time.Millisecond)
}

// For ROM bootloaders, skip flash_begin/flash_end — sending
// CMD_FLASH_BEGIN after a compressed download may interfere with
// the flash controller state at offset 0.
// Chips with a chip-specific hard reset (e.g. the ESP32-S2 USB-OTG
// watchdog reset) use it here; if it's unavailable or returns false,
// fall through to the DTR/RTS toggling below.
if f.chip != nil && f.chip.HardResetOTG != nil && f.chip.HardResetOTG(f) {
f.logf("Device reset.")
return
}

if f.usesUSB {
hardResetUSB(f.port)
} else {
Expand Down
78 changes: 78 additions & 0 deletions pkg/espflasher/reset_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package espflasher

import (
"errors"
"testing"
"time"

Expand Down Expand Up @@ -212,3 +213,80 @@ func TestHardResetUSBDeassertsDTRFirst(t *testing.T) {
assert.Equal(t, "DTR", first.line, "first call must be SetDTR on USB path")
assert.False(t, first.value, "first SetDTR must be false (release GPIO0)")
}

// TestFlasherResetESP32S2UsesWatchdog verifies that Flasher.Reset() takes
// the chip's HardResetOTG hook (RTC watchdog) for an ESP32-S2 flasher over
// native USB-OTG, instead of the DTR/RTS hardResetUSB path used by chips
// with a USB-Serial-JTAG bridge.
func TestFlasherResetESP32S2UsesWatchdog(t *testing.T) {
port := &recordingPort{}
mc := &mockConnection{
readRegFunc: func(addr uint32) (uint32, error) {
return 0, nil // strap clear, force-download not set
},
writeRegFunc: func(addr, value, mask, delayUS uint32) error {
return nil
},
}
f := &Flasher{
conn: mc,
port: port,
opts: &FlasherOptions{},
chip: defESP32S2,
usesUSB: true,
}

f.Reset()

assert.Empty(t, port.calls, "watchdog reset path must not toggle DTR/RTS")
}

// TestFlasherResetESP32S2WatchdogWriteFailureFallsBack verifies that when a
// watchdog register write fails, Flasher.Reset() falls back to the DTR/RTS
// hardResetUSB path instead of treating the failed watchdog arm as a
// successful reset.
func TestFlasherResetESP32S2WatchdogWriteFailureFallsBack(t *testing.T) {
port := &recordingPort{}
mc := &mockConnection{
readRegFunc: func(addr uint32) (uint32, error) {
return 0, nil // strap clear, force-download not set
},
writeRegFunc: func(addr, value, mask, delayUS uint32) error {
if addr == esp32s2RTCCntlWDTConfig0 {
return errors.New("write failed")
}
return nil
},
}
f := &Flasher{
conn: mc,
port: port,
opts: &FlasherOptions{},
chip: defESP32S2,
usesUSB: true,
}

f.Reset()

assert.NotEmpty(t, port.calls, "watchdog write failure must fall back to DTR/RTS reset")
}

// TestFlasherResetUSBJTAGChipUnchanged verifies that chips using the
// USB-Serial-JTAG bridge (no HardResetOTG hook, e.g. S3/C3/C6/H2/C5) still
// take the existing DTR/RTS hardResetUSB path, unaffected by the S2
// watchdog-reset addition.
func TestFlasherResetUSBJTAGChipUnchanged(t *testing.T) {
port := &recordingPort{}
mc := &mockConnection{}
f := &Flasher{
conn: mc,
port: port,
opts: &FlasherOptions{},
chip: defESP32S3,
usesUSB: true,
}

f.Reset()

assert.NotEmpty(t, port.calls, "USB-Serial-JTAG chips must still use the DTR/RTS reset path")
}
103 changes: 100 additions & 3 deletions pkg/espflasher/target_esp32s2.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,46 @@
package espflasher

// ESP32-S2 register addresses for USB interface detection.
// Reference: esptool/targets/esp32s2.py
import (
"fmt"
"time"
)

// ESP32-S2 register addresses for USB interface detection and the RTC
// watchdog reset used to exit native USB-OTG download mode.
// Reference: esptool/targets/esp32s2.py (ESP32S2ROM)
const (
esp32s2UARTDevBufNo uint32 = 0x3FFFFD14 // ROM .bss: active console interface
esp32s2UARTDevBufNoUSBOTG uint32 = 2 // USB-OTG active

// RTC_CNTL watchdog registers used by watchdog_reset() to force a
// system reset. ESP32-S2 has no USB-Serial-JTAG bridge, so the
// DTR/RTS latch trick used on S3/C3/C6/H2/C5 (hardResetUSB) is a
// no-op here; esptool instead arms and lets the RTC WDT fire.
esp32s2RTCCntlWDTConfig0 uint32 = 0x3F408094
esp32s2RTCCntlWDTConfig1 uint32 = 0x3F408098
esp32s2RTCCntlWDTWProtect uint32 = 0x3F4080AC
esp32s2RTCCntlWDTWKey uint32 = 0x50D83AA1

// esp32s2WDTConfig0EnableValue is the RTC_CNTL_WDTCONFIG0 value used to
// arm the watchdog for a system reset. It is copied verbatim from
// esptool's ESP32S2ROM.watchdog_reset() and treated as an opaque,
// HW-validated magic value rather than a precise bitfield breakdown of
// the ESP32-S2 TRM register layout.
esp32s2WDTConfig0EnableValue uint32 = (1 << 31) | (5 << 28) | (1 << 8) | 2
// esp32s2WDTConfig1TimeoutTicks is the stage 0 timeout, in RTC_CLK
// ticks (esptool uses 2000, i.e. a fast timeout since XTAL/RTC_CLK
// runs the RTC watchdog).
esp32s2WDTConfig1TimeoutTicks uint32 = 2000

// GPIO strapping / RTC_CNTL_OPTION1 registers used to gate the
// watchdog reset: if the chip is strapped for download boot, or
// download mode is force-enabled, a watchdog reset would just put
// it right back into the bootloader, so the caller must fall back
// to the DTR/RTS path instead.
esp32s2GPIOStrapReg uint32 = 0x3F404038
esp32s2GPIOStrapSPIBootMask uint32 = 1 << 3
esp32s2RTCCntlOption1Reg uint32 = 0x3F408128
esp32s2RTCCntlForceDownloadBoot uint32 = 0x1
)

// ESP32-S2 target definition.
Expand Down Expand Up @@ -49,7 +85,8 @@ var defESP32S2 = &chipDef{

FlashSizes: defaultFlashSizes(),

PostConnect: esp32s2PostConnect,
PostConnect: esp32s2PostConnect,
HardResetOTG: esp32s2HardReset,
}

// esp32s2PostConnect detects the USB interface type.
Expand All @@ -71,3 +108,63 @@ func esp32s2PostConnect(f *Flasher) error {

return nil
}

// esp32s2HardReset attempts an RTC watchdog reset to exit native USB-OTG
// download mode, mirroring esptool's ESP32S2ROM.hard_reset(). It is only
// applicable when the USB-OTG interface was detected (f.usesUSB); it
// returns false (fall back to the DTR/RTS hardReset path) if the interface
// isn't OTG, if the strap/force-download safety gate indicates a watchdog
// reset wouldn't reliably exit download mode, or if the register access
// fails (e.g. secure download mode).
func esp32s2HardReset(f *Flasher) bool {
if !f.usesUSB {
return false
}

strap, err := f.ReadRegister(esp32s2GPIOStrapReg)
if err != nil {
return false
}
option1, err := f.ReadRegister(esp32s2RTCCntlOption1Reg)
if err != nil {
return false
}

if strap&esp32s2GPIOStrapSPIBootMask != 0 || option1&esp32s2RTCCntlForceDownloadBoot != 0 {
// GPIO0 is strapped high (SPI-boot strap set), or RTC_CNTL force-download-boot
// is set: either condition means a watchdog reset would not cleanly exit to
// the app and would land back in the bootloader instead, so fall back to
// the DTR/RTS reset path.
return false
}

if err := esp32s2WatchdogReset(f); err != nil {
f.logf("watchdog reset failed, falling back to DTR/RTS reset: %v", err)
return false
}

return true
}

// esp32s2WatchdogReset arms the RTC watchdog to force a system reset,
// mirroring esptool's ESP32S2ROM.watchdog_reset(). Reference:
// esptool/targets/esp32s2.py watchdog_reset().
func esp32s2WatchdogReset(f *Flasher) error {
if err := f.WriteRegister(esp32s2RTCCntlWDTWProtect, esp32s2RTCCntlWDTWKey); err != nil {
return fmt.Errorf("unlock RTC WDT: %w", err)
}
if err := f.WriteRegister(esp32s2RTCCntlWDTConfig1, esp32s2WDTConfig1TimeoutTicks); err != nil {
return fmt.Errorf("set RTC WDT timeout: %w", err)
}
if err := f.WriteRegister(esp32s2RTCCntlWDTConfig0, esp32s2WDTConfig0EnableValue); err != nil {
return fmt.Errorf("enable RTC WDT: %w", err)
}
if err := f.WriteRegister(esp32s2RTCCntlWDTWProtect, 0); err != nil {
return fmt.Errorf("lock RTC WDT: %w", err)
}

f.logf("Hard resetting with a watchdog...")
time.Sleep(500 * time.Millisecond) // wait for reset to take effect

return nil
}
Loading
Loading