diff --git a/.github/workflows/hil.yml b/.github/workflows/hil.yml index 3763b4c..ffca619 100644 --- a/.github/workflows/hil.yml +++ b/.github/workflows/hil.yml @@ -134,4 +134,4 @@ jobs: run: timeout 20 bash espflash/tests/scripts/write-bin.sh - name: read-flash test - run: timeout 20 bash espflash/tests/scripts/read-flash.sh + run: timeout 30 bash espflash/tests/scripts/read-flash.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e0868..fe82c3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [cargo-espflash]: Add `write-bin` subcommand (#789) - Add `--monitor` option to `write-bin`. (#783) - Add `watchdog-reset` strategy to `--after` subcommand (#779) +- Add `ROM` version of `read-flash` command (#812) ### Changed diff --git a/espflash/src/cli/mod.rs b/espflash/src/cli/mod.rs index 85eb850..0d87298 100644 --- a/espflash/src/cli/mod.rs +++ b/espflash/src/cli/mod.rs @@ -850,19 +850,26 @@ fn erase_partition(flasher: &mut Flasher, part: &Partition) -> Result<()> { /// Read flash content and write it to a file pub fn read_flash(args: ReadFlashArgs, config: &Config) -> Result<()> { - if args.connect_args.no_stub { - return Err(Error::StubRequired.into()); - } - let mut flasher = connect(&args.connect_args, config, false, false)?; print_board_info(&mut flasher)?; - flasher.read_flash( - args.address, - args.size, - args.block_size, - args.max_in_flight, - args.file, - )?; + + if args.connect_args.no_stub { + flasher.read_flash_rom( + args.address, + args.size, + args.block_size, + args.max_in_flight, + args.file, + )?; + } else { + flasher.read_flash( + args.address, + args.size, + args.block_size, + args.max_in_flight, + args.file, + )?; + } Ok(()) } diff --git a/espflash/src/connection/command.rs b/espflash/src/connection/command.rs index 5dc6ee4..f53323f 100644 --- a/espflash/src/connection/command.rs +++ b/espflash/src/connection/command.rs @@ -56,6 +56,7 @@ pub enum CommandType { EraseFlash = 0xD0, EraseRegion = 0xD1, ReadFlash = 0xD2, + ReadFlashSlow = 0x0E, // ROM only, much slower than the stub read_flash RunUserCode = 0xD3, // Flash encryption debug mode supported command FlashEncryptedData = 0xD4, @@ -189,6 +190,12 @@ pub enum Command<'a> { block_size: u32, max_in_flight: u32, }, + ReadFlashSlow { + offset: u32, + size: u32, + block_size: u32, + max_in_flight: u32, + }, RunUserCode, FlashDetect, GetSecurityInfo, @@ -218,6 +225,7 @@ impl Command<'_> { Command::EraseFlash { .. } => CommandType::EraseFlash, Command::EraseRegion { .. } => CommandType::EraseRegion, Command::ReadFlash { .. } => CommandType::ReadFlash, + Command::ReadFlashSlow { .. } => CommandType::ReadFlashSlow, Command::RunUserCode { .. } => CommandType::RunUserCode, Command::FlashDetect => CommandType::FlashDetect, Command::GetSecurityInfo => CommandType::GetSecurityInfo, @@ -417,6 +425,22 @@ impl Command<'_> { writer.write_all(&block_size.to_le_bytes())?; writer.write_all(&(max_in_flight.to_le_bytes()))?; } + Command::ReadFlashSlow { + offset, + size, + block_size, + max_in_flight, + } => { + // length + writer.write_all(&(16u16.to_le_bytes()))?; + // checksum + writer.write_all(&(0u32.to_le_bytes()))?; + // data + writer.write_all(&offset.to_le_bytes())?; + writer.write_all(&size.to_le_bytes())?; + writer.write_all(&block_size.to_le_bytes())?; + writer.write_all(&(max_in_flight.to_le_bytes()))?; + } Command::RunUserCode => { write_basic(writer, &[], 0)?; } diff --git a/espflash/src/flasher/mod.rs b/espflash/src/flasher/mod.rs index 4272d8e..97eda27 100644 --- a/espflash/src/flasher/mod.rs +++ b/espflash/src/flasher/mod.rs @@ -1214,6 +1214,63 @@ impl Flasher { Ok(()) } + pub fn read_flash_rom( + &mut self, + offset: u32, + size: u32, + block_size: u32, + max_in_flight: u32, + file_path: PathBuf, + ) -> Result<(), Error> { + // ROM read limit per command + const BLOCK_LEN: usize = 64; + + let mut data: Vec = Vec::new(); + + let mut file = fs::OpenOptions::new() + .write(true) + .truncate(true) + .create(true) + .open(&file_path)?; + + let mut correct_offset = offset; + + while data.len() < size as usize { + let block_len = std::cmp::min(BLOCK_LEN, size as usize - data.len()); + + correct_offset += data.len() as u32; + + let response = self.connection.with_timeout( + CommandType::ReadFlashSlow.timeout(), + |connection| { + connection.command(Command::ReadFlashSlow { + offset: correct_offset, + size: block_len as u32, + block_size, + max_in_flight, + }) + }, + )?; + + let payload: Vec = response.try_into()?; + + assert!(payload.len() >= block_len); + + // command always returns 64 byte buffer, + // regardless of how many bytes were actually read from flash + data.append(&mut payload[..block_len].to_vec()); + } + + file.write_all(&data)?; + + info!( + "Flash content successfully read and written to '{}'!", + file_path.display() + ); + + Ok(()) + } + pub fn read_flash( &mut self, offset: u32, diff --git a/espflash/tests/scripts/read-flash.sh b/espflash/tests/scripts/read-flash.sh index 365a104..9a400a8 100644 --- a/espflash/tests/scripts/read-flash.sh +++ b/espflash/tests/scripts/read-flash.sh @@ -33,6 +33,21 @@ for len in "${lengths[@]}"; do echo "Verification failed: content does not match expected for length" exit 1 fi + + echo "Testing ROM read-flash with length: $len" + result=$(espflash read-flash --no-stub 0 "$len" flash_content.bin 2>&1) + echo "$result" + + if ! cmp -s <(echo -ne "$EXPECTED") flash_content.bin; then + echo "Verification failed: content does not match expected for length" + exit 1 + fi + + if [[ ! $result =~ "Flash content successfully read and written to" ]]; then + echo "Failed to read $len bytes from flash" + exit 1 + fi + done echo "All read-flash tests passed!"