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
7 changes: 6 additions & 1 deletion pkg/espflasher/reset.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,18 @@ func hardReset(port serial.Port, usesUSB bool) {
// not bootloader mode.
port.SetDTR(false) //nolint:errcheck
}
port.SetRTS(true) //nolint:errcheck
port.SetRTS(true) //nolint:errcheck // EN=LOW (chip in reset)
if usesUSB {
time.Sleep(200 * time.Millisecond)
port.SetRTS(false) //nolint:errcheck
time.Sleep(200 * time.Millisecond)
} else {
time.Sleep(100 * time.Millisecond)
// Release DTR before exiting reset. Otherwise a leftover DTR=true
// from a prior operation holds IO0 LOW at reset exit and the chip
// boots into the download-mode bootloader instead of the
// application. Matches esptool.py HardReset.
port.SetDTR(false) //nolint:errcheck
port.SetRTS(false) //nolint:errcheck
}
}
Expand Down
57 changes: 54 additions & 3 deletions pkg/espflasher/reset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,29 @@ import (
)

// recordingPort tracks all calls to SetDTR and SetRTS for testing.
// Each call is recorded as a separate event to allow testing the order and
// combinations of line state transitions.
// Separate dtrCalls/rtsCalls slices preserve the per-line value history;
// the unified calls slice preserves the full cross-line ordering needed
// by tests that assert on interleaving.
type recordingPort struct {
dtrCalls []bool
rtsCalls []bool
calls []lineCall
}

type lineCall struct {
line string // "DTR" or "RTS"
value bool
}

func (r *recordingPort) SetDTR(dtr bool) error {
r.dtrCalls = append(r.dtrCalls, dtr)
r.calls = append(r.calls, lineCall{line: "DTR", value: dtr})
return nil
}

func (r *recordingPort) SetRTS(rts bool) error {
r.rtsCalls = append(r.rtsCalls, rts)
r.calls = append(r.calls, lineCall{line: "RTS", value: rts})
return nil
}

Expand All @@ -35,10 +44,19 @@ func (r *recordingPort) SetWriteTimeout(t time.Duration) error {
func (r *recordingPort) Close() error { return nil }
func (r *recordingPort) ResetInputBuffer() error { return nil }
func (r *recordingPort) ResetOutputBuffer() error { return nil }
func (r *recordingPort) GetModemStatusBits() (*serial.ModemStatusBits, error) { return nil, nil }
func (r *recordingPort) GetModemStatusBits() (*serial.ModemStatusBits, error) { return nil, nil }
func (r *recordingPort) Break(t time.Duration) error { return nil }
func (r *recordingPort) Drain() error { return nil }

func indexOf(calls []lineCall, line string, value bool, startAt int) int {
for i := startAt; i < len(calls); i++ {
if calls[i].line == line && calls[i].value == value {
return i
}
}
return -1
}

// TestClassicReset verifies the classic reset sequence.
func TestClassicReset(t *testing.T) {
port := &recordingPort{}
Expand Down Expand Up @@ -161,3 +179,36 @@ func TestResetDelayConstants(t *testing.T) {
assert.Equal(t, 50*time.Millisecond, defaultResetDelay, "defaultResetDelay should be 50ms")
assert.Equal(t, 550*time.Millisecond, extraResetDelay, "extraResetDelay should be 550ms")
}

// TestHardResetNonUSBReleasesDTRBeforeReleasingReset verifies that on the
// non-USB path, hardReset deasserts DTR before releasing EN (RTS=false).
// Otherwise a leftover DTR=true from a prior operation holds IO0 LOW when
// EN goes HIGH and the chip re-enters the download-mode bootloader.
func TestHardResetNonUSBReleasesDTRBeforeReleasingReset(t *testing.T) {
port := &recordingPort{}
hardReset(port, false)

rtsTrue := indexOf(port.calls, "RTS", true, 0)
require := assert.New(t)
require.GreaterOrEqual(rtsTrue, 0, "expected SetRTS(true) to pull EN LOW")

dtrFalse := indexOf(port.calls, "DTR", false, rtsTrue)
require.Greater(dtrFalse, rtsTrue, "SetDTR(false) must happen after EN is pulled LOW")

rtsFalseFinal := indexOf(port.calls, "RTS", false, dtrFalse)
require.Greater(rtsFalseFinal, dtrFalse,
"final SetRTS(false) (release reset) must happen after SetDTR(false) so IO0 is HIGH when EN goes HIGH")
}

// TestHardResetUSBDeassertsDTRFirst verifies that on the USB-JTAG path,
// hardReset deasserts DTR before driving EN, so GPIO0 is HIGH (normal boot,
// not bootloader) at the moment the USB-JTAG peripheral latches the reset.
func TestHardResetUSBDeassertsDTRFirst(t *testing.T) {
port := &recordingPort{}
hardReset(port, true)

assert.NotEmpty(t, port.calls)
first := port.calls[0]
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)")
}
Loading