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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- eFuse write support (#962)
- Support flashing in secure download mode (#990)

### Changed

Expand Down
4 changes: 4 additions & 0 deletions espflash/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,10 @@ pub enum Error {
/// Tried to use an unsupported eFuse coding scheme
#[error("Tried to use an unsupported eFuse coding scheme: {0}")]
UnsupportedEfuseCodingScheme(String),

/// Tried to write to address < 0x8000 with Secure Download enabled
#[error("Tried to write to address < 0x8000 with Secure Download enabled")]
SecureDownloadBootloaderProtection,
}

#[cfg(feature = "serialport")]
Expand Down
50 changes: 40 additions & 10 deletions espflash/src/flasher/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ pub(crate) const TRY_SPI_PARAMS: [SpiAttachParams; 2] =
pub(crate) const FLASH_SECTOR_SIZE: usize = 0x1000;
pub(crate) const FLASH_WRITE_SIZE: usize = 0x400;

#[cfg(feature = "serialport")]
pub(crate) const BOOTLOADER_PROTECTION_ADDR: u32 = 0x8000;

/// Supported flash frequencies
///
/// Note that not all frequencies are supported by each target device.
Expand Down Expand Up @@ -924,9 +927,26 @@ impl Flasher {
progress: &mut dyn ProgressCallbacks,
image_format: ImageFormat<'a>,
) -> Result<(), Error> {
let mut target =
self.chip
.flash_target(self.spi_params, self.use_stub, self.verify, self.skip);
let (mut verify, mut skip) = (self.verify, self.skip);
if self.connection.secure_download_mode {
for segment in image_format.clone().flash_segments() {
if segment.addr < BOOTLOADER_PROTECTION_ADDR {
return Err(Error::SecureDownloadBootloaderProtection);
}
}
Comment on lines +932 to +936
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The image_format is cloned here to iterate segments for address validation, then the original is consumed later at line 964. This could be optimized by collecting segments once into a Vec, validating addresses, then flashing from the Vec. However, this would require changing how flash_segments works since it consumes self.

Consider refactoring to collect segments once and reuse them, or provide a non-consuming iterator method for flash_segments to avoid the clone.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is valid, but also begs the question whether we should split the check out into its own fn, as its duplicated in more than one place.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I ended up not breaking it out initially as load_image_to_flash() and write_bins_to_flash() are close to duplicates right now. I'm happy break this out into a new fn and open a new PR to track a unification of these and introduce a --force flag if desired).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can do the deduplication in this PR and leave the --force flag for an upcoming PR?


if self.verify || self.skip {
warn!(
"Secure Download Mode enabled: --verify and --skip options are not available \
(flash read operations are restricted)"
);
}
(verify, skip) = (false, false);
}

let mut target = self
.chip
.flash_target(self.spi_params, self.use_stub, verify, skip);
target.begin(&mut self.connection).flashing()?;

// When the `cli` feature is enabled, display the image size information.
Expand Down Expand Up @@ -987,16 +1007,26 @@ impl Flasher {
segments: &[Segment<'_>],
progress: &mut dyn ProgressCallbacks,
) -> Result<(), Error> {
let (mut verify, mut skip) = (self.verify, self.skip);
if self.connection.secure_download_mode {
return Err(Error::UnsupportedFeature {
chip: self.chip,
feature: "writing binaries in Secure Download Mode currently".into(),
});
for segment in segments {
if segment.addr < BOOTLOADER_PROTECTION_ADDR {
return Err(Error::SecureDownloadBootloaderProtection);
}
}

if self.verify || self.skip {
warn!(
"Secure Download Mode enabled: --verify and --skip options are not available \
(flash read operations are restricted)"
);
}
(verify, skip) = (false, false);
}

let mut target =
self.chip
.flash_target(self.spi_params, self.use_stub, self.verify, self.skip);
let mut target = self
.chip
.flash_target(self.spi_params, self.use_stub, verify, skip);

target.begin(&mut self.connection).flashing()?;

Expand Down
140 changes: 95 additions & 45 deletions espflash/src/target/flash_target/esp32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! This module defines the traits and types used for flashing operations on a
//! target device's flash memory.

use std::io::Write;
use std::{io::Write, iter::repeat_n};

use flate2::{
Compression,
Expand Down Expand Up @@ -34,7 +34,7 @@ pub struct Esp32Target {
use_stub: bool,
verify: bool,
skip: bool,
need_deflate_end: bool,
need_flash_end: bool,
}

impl Esp32Target {
Expand All @@ -52,7 +52,7 @@ impl Esp32Target {
use_stub,
verify,
skip,
need_deflate_end: false,
need_flash_end: false,
}
}
}
Expand All @@ -79,7 +79,10 @@ impl FlashTarget for Esp32Target {
//
// TODO: the stub doesn't appear to disable the watchdog on ESP32-S3, so we
// explicitly disable the watchdog here.
if connection.is_using_usb_serial_jtag() {
//
// NOTE: In Secure Download Mode, WRITE_REG commands are not allowed, so we
// must skip the watchdog disable.
if connection.is_using_usb_serial_jtag() && !connection.secure_download_mode {
if let (Some(wdt_wprotect), Some(wdt_config0)) =
(self.chip.wdt_wprotect(), self.chip.wdt_config0())
{
Expand Down Expand Up @@ -116,22 +119,15 @@ impl FlashTarget for Esp32Target {
md5_hasher.update(&segment.data);
let checksum_md5 = md5_hasher.finalize();

let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
encoder.write_all(&segment.data)?;
let compressed = encoder.finish()?;
// use compression only when stub is loaded.
let use_compression = self.use_stub;

let flash_write_size = self.chip.flash_write_size();
let block_count = compressed.len().div_ceil(flash_write_size);
let erase_count = segment.data.len().div_ceil(FLASH_SECTOR_SIZE);

// round up to sector size
let erase_size = (erase_count * FLASH_SECTOR_SIZE) as u32;

let chunks = compressed.chunks(flash_write_size);
let num_chunks = chunks.len();

progress.init(addr, num_chunks);

if self.skip {
let flash_checksum_md5: u128 = connection.with_timeout(
CommandType::FlashMd5.timeout_for_size(segment.data.len() as u32),
Expand All @@ -153,43 +149,91 @@ impl FlashTarget for Esp32Target {
}
}

connection.with_timeout(
CommandType::FlashDeflBegin.timeout_for_size(erase_size),
|connection| {
connection.command(Command::FlashDeflBegin {
size: segment.data.len() as u32,
blocks: block_count as u32,
block_size: flash_write_size as u32,
offset: addr,
supports_encryption: self.chip != Chip::Esp32 && !self.use_stub,
})?;
Ok(())
},
)?;
self.need_deflate_end = true;
let data = if use_compression {
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
encoder.write_all(&segment.data)?;
encoder.finish()?
} else {
let mut data = segment.data.to_vec();
let padding = (flash_write_size - (data.len() % flash_write_size)) % flash_write_size;
data.extend(repeat_n(0xff, padding));
data
Comment on lines +157 to +160
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When compression is disabled, the data is manually padded here with 0xff bytes to align to flash_write_size. However, at line 229, the FlashData command is also configured with pad_to: flash_write_size, which causes the data_command function to apply padding again. This results in double padding.

The manual padding should be removed since the FlashData command handles padding automatically via the pad_to parameter.

Copilot uses AI. Check for mistakes.
};

// decode the chunks to see how much data the device will have to save
let mut decoder = ZlibDecoder::new(Vec::new());
let mut decoded_size = 0;
let block_count = data.len().div_ceil(flash_write_size);
let chunks = data.chunks(flash_write_size);
let num_chunks = chunks.len();

for (i, block) in chunks.enumerate() {
decoder.write_all(block)?;
decoder.flush()?;
let size = decoder.get_ref().len() - decoded_size;
decoded_size = decoder.get_ref().len();
progress.init(addr, num_chunks);

if use_compression {
connection.with_timeout(
CommandType::FlashDeflBegin.timeout_for_size(erase_size),
|connection| {
connection.command(Command::FlashDeflBegin {
size: segment.data.len() as u32,
blocks: block_count as u32,
block_size: flash_write_size as u32,
offset: addr,
supports_encryption: self.chip != Chip::Esp32 && !self.use_stub,
})?;
Ok(())
},
)?;
self.need_flash_end = true;
} else {
connection.with_timeout(
CommandType::FlashDeflData.timeout_for_size(size as u32),
CommandType::FlashBegin.timeout_for_size(erase_size),
|connection| {
connection.command(Command::FlashDeflData {
sequence: i as u32,
pad_to: 0,
pad_byte: 0xff,
data: block,
connection.command(Command::FlashBegin {
size: erase_size,
blocks: block_count as u32,
block_size: flash_write_size as u32,
offset: addr,
supports_encryption: self.chip != Chip::Esp32,
})?;
Ok(())
},
)?;
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When FlashBegin is called (non-compression path), need_flash_end should be set to true to ensure FlashEnd is called in the finish() method. Without this, the flash operation won't be properly completed in Secure Download Mode.

Add self.need_flash_end = true; after the FlashBegin command, similar to what's done for the compression path at line 183.

Suggested change
)?;
)?;
self.need_flash_end = true;

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is absolutely valid, messed up my rebase and caused this.

}

// decode the chunks to see how much data the device will have to save
let mut decoder = ZlibDecoder::new(Vec::new());
let mut decoded_size = 0;

for (i, block) in chunks.enumerate() {
if use_compression {
decoder.write_all(block)?;
decoder.flush()?;
let size = decoder.get_ref().len() - decoded_size;
decoded_size = decoder.get_ref().len();

connection.with_timeout(
CommandType::FlashDeflData.timeout_for_size(size as u32),
|connection| {
connection.command(Command::FlashDeflData {
sequence: i as u32,
pad_to: 0,
pad_byte: 0xff,
data: block,
})?;
Ok(())
},
)?;
} else {
connection.with_timeout(
CommandType::FlashData.timeout_for_size(block.len() as u32),
|connection| {
connection.command(Command::FlashData {
sequence: i as u32,
pad_to: flash_write_size,
pad_byte: 0xff,
data: block,
})?;
Ok(())
},
)?;
}

progress.update(i + 1)
}
Expand Down Expand Up @@ -220,10 +264,16 @@ impl FlashTarget for Esp32Target {
}

fn finish(&mut self, connection: &mut Connection, reboot: bool) -> Result<(), Error> {
if self.need_deflate_end {
connection.with_timeout(CommandType::FlashDeflEnd.timeout(), |connection| {
connection.command(Command::FlashDeflEnd { reboot: false })
})?;
if self.need_flash_end {
if self.use_stub {
connection.with_timeout(CommandType::FlashDeflEnd.timeout(), |connection| {
connection.command(Command::FlashDeflEnd { reboot: false })
})?;
} else {
connection.with_timeout(CommandType::FlashEnd.timeout(), |connection| {
connection.command(Command::FlashEnd { reboot: false })
})?;
}
}

if reboot {
Expand Down