diff --git a/pkg/espflasher/protocol.go b/pkg/espflasher/protocol.go index d3cf58c..dc67350 100644 --- a/pkg/espflasher/protocol.go +++ b/pkg/espflasher/protocol.go @@ -76,6 +76,7 @@ const ( chipEraseTimeout = 120 * time.Second md5Timeout = 30 * time.Second eraseWritePerMBRate = 10 * time.Second // per megabyte + flashWritePerMBRate = 40 * time.Second // per megabyte ) // conn wraps the serial port and provides the low-level protocol operations. @@ -424,7 +425,8 @@ func (c *conn) flashData(block []byte, seq uint32) error { binary.LittleEndian.PutUint32(data[12:16], 0) copy(data[16:], block) - _, err := c.checkCommand("write flash block", cmdFlashData, data, checksum(block), defaultTimeout, 0) + timeout := flashWriteTimeoutForSize(uint32(len(block))) + _, err := c.checkCommand("write flash block", cmdFlashData, data, checksum(block), timeout, 0) return err } @@ -675,13 +677,14 @@ func md5TimeoutForSize(size uint32) time.Duration { } // flashWriteTimeoutForSize calculates an appropriate ack timeout for -// compressed flash write/finish commands, scaled by data size using the -// same per-MB rate as eraseTimeoutForSize. Unlike erase (which floors at -// 10s for a whole-region operation), write/finish acks are per-block or -// per-image and use the smaller defaultTimeout floor so small writes -// aren't over-inflated. +// flash write/finish commands, scaled by data size. Flash writes (and +// especially compressed writes that must be decompressed before being +// programmed) can take substantially longer than the erase rate on slower +// ESP32 processors, so this uses a more generous per-MB rate than +// eraseTimeoutForSize. The floor remains defaultTimeout so small blocks +// are not over-inflated. func flashWriteTimeoutForSize(size uint32) time.Duration { - t := defaultTimeout + time.Duration(float64(eraseWritePerMBRate)*float64(size)/float64(1024*1024)) + t := defaultTimeout + time.Duration(float64(flashWritePerMBRate)*float64(size)/float64(1024*1024)) if t < defaultTimeout { t = defaultTimeout } diff --git a/pkg/espflasher/protocol_test.go b/pkg/espflasher/protocol_test.go index 741bda3..89abd76 100644 --- a/pkg/espflasher/protocol_test.go +++ b/pkg/espflasher/protocol_test.go @@ -110,9 +110,10 @@ func TestEraseTimeoutForSize(t *testing.T) { } func TestFlashWriteTimeoutForSize(t *testing.T) { - // E1-15 regression: compressed flash write/finish acks must scale by - // size (mirroring eraseTimeoutForSize), with a floor of defaultTimeout - // so small blocks aren't over-inflated. + // E1-15 regression: flash write/finish acks must scale by size, with a + // floor of defaultTimeout so small blocks aren't over-inflated. Flash + // writes use a more generous per-MB rate than erase because the stub + // must decompress and program each block before acking. tests := []struct { name string size uint32 @@ -131,9 +132,9 @@ func TestFlashWriteTimeoutForSize(t *testing.T) { }) } - // A 4MB compressed write must get a materially larger timeout than a - // 4KB block, so large transfers over slow USB-serial bridges don't - // time out on a flat ack wait. + // A 4MB write must get a materially larger timeout than a 4KB block, + // so large transfers over slow USB-serial bridges don't time out on a + // flat ack wait. small := flashWriteTimeoutForSize(4 * 1024) large := flashWriteTimeoutForSize(4 * 1024 * 1024) if large <= small { @@ -142,6 +143,14 @@ func TestFlashWriteTimeoutForSize(t *testing.T) { if large < 2*small { t.Errorf("expected 4MB timeout to be materially larger than 4KB timeout: small=%v large=%v", small, large) } + + // Flash writes are given a longer per-MB budget than erase to account + // for decompression + programming on slower ESP32 processors. + erase := eraseTimeoutForSize(1024 * 1024) + write := flashWriteTimeoutForSize(1024 * 1024) + if write <= erase { + t.Errorf("expected flash write timeout > erase timeout for same size: erase=%v write=%v", erase, write) + } } func TestSendCommandFormat(t *testing.T) {