Skip to content

Commit 28646c3

Browse files
jgangemideadprogram
authored andcommitted
fix: release DTR in hardReset so chip boots the application
When the prior operation left DTR=true (IO0 held LOW on typical UART bridges), hardReset only toggled RTS and released the chip from reset with IO0 still LOW, sending it back into the download-mode bootloader instead of the application. esptool.py's HardReset explicitly deasserts DTR between the EN=LOW and EN=HIGH transitions. Mirror that: set DTR(false) before the final RTS(false) on the non-USB path.
1 parent 5a91087 commit 28646c3

2 files changed

Lines changed: 60 additions & 4 deletions

File tree

pkg/espflasher/reset.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,13 +168,18 @@ func hardReset(port serial.Port, usesUSB bool) {
168168
// not bootloader mode.
169169
port.SetDTR(false) //nolint:errcheck
170170
}
171-
port.SetRTS(true) //nolint:errcheck
171+
port.SetRTS(true) //nolint:errcheck // EN=LOW (chip in reset)
172172
if usesUSB {
173173
time.Sleep(200 * time.Millisecond)
174174
port.SetRTS(false) //nolint:errcheck
175175
time.Sleep(200 * time.Millisecond)
176176
} else {
177177
time.Sleep(100 * time.Millisecond)
178+
// Release DTR before exiting reset. Otherwise a leftover DTR=true
179+
// from a prior operation holds IO0 LOW at reset exit and the chip
180+
// boots into the download-mode bootloader instead of the
181+
// application. Matches esptool.py HardReset.
182+
port.SetDTR(false) //nolint:errcheck
178183
port.SetRTS(false) //nolint:errcheck
179184
}
180185
}

pkg/espflasher/reset_test.go

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,29 @@ import (
1010
)
1111

1212
// recordingPort tracks all calls to SetDTR and SetRTS for testing.
13-
// Each call is recorded as a separate event to allow testing the order and
14-
// combinations of line state transitions.
13+
// Separate dtrCalls/rtsCalls slices preserve the per-line value history;
14+
// the unified calls slice preserves the full cross-line ordering needed
15+
// by tests that assert on interleaving.
1516
type recordingPort struct {
1617
dtrCalls []bool
1718
rtsCalls []bool
19+
calls []lineCall
20+
}
21+
22+
type lineCall struct {
23+
line string // "DTR" or "RTS"
24+
value bool
1825
}
1926

2027
func (r *recordingPort) SetDTR(dtr bool) error {
2128
r.dtrCalls = append(r.dtrCalls, dtr)
29+
r.calls = append(r.calls, lineCall{line: "DTR", value: dtr})
2230
return nil
2331
}
2432

2533
func (r *recordingPort) SetRTS(rts bool) error {
2634
r.rtsCalls = append(r.rtsCalls, rts)
35+
r.calls = append(r.calls, lineCall{line: "RTS", value: rts})
2736
return nil
2837
}
2938

@@ -35,10 +44,19 @@ func (r *recordingPort) SetWriteTimeout(t time.Duration) error {
3544
func (r *recordingPort) Close() error { return nil }
3645
func (r *recordingPort) ResetInputBuffer() error { return nil }
3746
func (r *recordingPort) ResetOutputBuffer() error { return nil }
38-
func (r *recordingPort) GetModemStatusBits() (*serial.ModemStatusBits, error) { return nil, nil }
47+
func (r *recordingPort) GetModemStatusBits() (*serial.ModemStatusBits, error) { return nil, nil }
3948
func (r *recordingPort) Break(t time.Duration) error { return nil }
4049
func (r *recordingPort) Drain() error { return nil }
4150

51+
func indexOf(calls []lineCall, line string, value bool, startAt int) int {
52+
for i := startAt; i < len(calls); i++ {
53+
if calls[i].line == line && calls[i].value == value {
54+
return i
55+
}
56+
}
57+
return -1
58+
}
59+
4260
// TestClassicReset verifies the classic reset sequence.
4361
func TestClassicReset(t *testing.T) {
4462
port := &recordingPort{}
@@ -161,3 +179,36 @@ func TestResetDelayConstants(t *testing.T) {
161179
assert.Equal(t, 50*time.Millisecond, defaultResetDelay, "defaultResetDelay should be 50ms")
162180
assert.Equal(t, 550*time.Millisecond, extraResetDelay, "extraResetDelay should be 550ms")
163181
}
182+
183+
// TestHardResetNonUSBReleasesDTRBeforeReleasingReset verifies that on the
184+
// non-USB path, hardReset deasserts DTR before releasing EN (RTS=false).
185+
// Otherwise a leftover DTR=true from a prior operation holds IO0 LOW when
186+
// EN goes HIGH and the chip re-enters the download-mode bootloader.
187+
func TestHardResetNonUSBReleasesDTRBeforeReleasingReset(t *testing.T) {
188+
port := &recordingPort{}
189+
hardReset(port, false)
190+
191+
rtsTrue := indexOf(port.calls, "RTS", true, 0)
192+
require := assert.New(t)
193+
require.GreaterOrEqual(rtsTrue, 0, "expected SetRTS(true) to pull EN LOW")
194+
195+
dtrFalse := indexOf(port.calls, "DTR", false, rtsTrue)
196+
require.Greater(dtrFalse, rtsTrue, "SetDTR(false) must happen after EN is pulled LOW")
197+
198+
rtsFalseFinal := indexOf(port.calls, "RTS", false, dtrFalse)
199+
require.Greater(rtsFalseFinal, dtrFalse,
200+
"final SetRTS(false) (release reset) must happen after SetDTR(false) so IO0 is HIGH when EN goes HIGH")
201+
}
202+
203+
// TestHardResetUSBDeassertsDTRFirst verifies that on the USB-JTAG path,
204+
// hardReset deasserts DTR before driving EN, so GPIO0 is HIGH (normal boot,
205+
// not bootloader) at the moment the USB-JTAG peripheral latches the reset.
206+
func TestHardResetUSBDeassertsDTRFirst(t *testing.T) {
207+
port := &recordingPort{}
208+
hardReset(port, true)
209+
210+
assert.NotEmpty(t, port.calls)
211+
first := port.calls[0]
212+
assert.Equal(t, "DTR", first.line, "first call must be SetDTR on USB path")
213+
assert.False(t, first.value, "first SetDTR must be false (release GPIO0)")
214+
}

0 commit comments

Comments
 (0)