diff --git a/cargo-espflash/src/main.rs b/cargo-espflash/src/main.rs index c4a610db..eaa79936 100644 --- a/cargo-espflash/src/main.rs +++ b/cargo-espflash/src/main.rs @@ -1,11 +1,11 @@ use anyhow::{anyhow, bail, Context}; use cargo_metadata::Message; use clap::{App, Arg, SubCommand}; -use espflash::{Config, Flasher}; +use espflash::{Config, Flasher, PartitionTable}; use serial::{BaudRate, SerialPort}; use std::{ - fs::read, + fs, path::PathBuf, process::{exit, Command, ExitStatus, Stdio}, string::ToString, @@ -51,6 +51,13 @@ fn main() -> anyhow::Result<()> { .value_name("FEATURES") .help("Comma delimited list of build features"), ) + .arg( + Arg::with_name("partition_table") + .long("partition-table") + .takes_value(true) + .value_name("PATH") + .help("Path to a CSV file containing partition table"), + ) .arg( Arg::with_name("speed") .long("speed") @@ -128,12 +135,26 @@ fn main() -> anyhow::Result<()> { return Ok(()); } + // If the '--partition-table' option is provided, load the partition table from + // the CSV at the specified path. + let partition_table = if let Some(path) = matches.value_of("partition_table") { + let path = fs::canonicalize(path)?; + let data = fs::read_to_string(path)?; + + match PartitionTable::try_from_str(data) { + Ok(t) => Some(t), + Err(e) => bail!("{}", e), + } + } else { + None + }; + // Read the ELF data from the build path and load it to the target. - let elf_data = read(path.unwrap())?; + let elf_data = fs::read(path.unwrap())?; if matches.is_present("ram") { flasher.load_elf_to_ram(&elf_data)?; } else { - flasher.load_elf_to_flash(&elf_data)?; + flasher.load_elf_to_flash(&elf_data, partition_table)?; } // We're all done! diff --git a/espflash/Cargo.toml b/espflash/Cargo.toml index 69cc760a..18c987a6 100644 --- a/espflash/Cargo.toml +++ b/espflash/Cargo.toml @@ -30,6 +30,8 @@ directories-next = "2.0.0" color-eyre = "0.5" strum = "0.21.0" strum_macros = "0.21.1" +csv = "1.1.6" +regex = "1.5.4" [dev-dependencies] pretty_assertions = "0.7.1" diff --git a/espflash/src/chip/esp32.rs b/espflash/src/chip/esp32.rs index 46b00b3b..33138284 100644 --- a/espflash/src/chip/esp32.rs +++ b/espflash/src/chip/esp32.rs @@ -8,8 +8,7 @@ use crate::{ WP_PIN_DISABLED, }, elf::{FirmwareImage, RomSegment, ESP_CHECKSUM_MAGIC}, - partition_table::PartitionTable, - Error, + Error, PartitionTable, }; use std::{borrow::Cow, io::Write, iter::once}; @@ -52,18 +51,23 @@ impl ChipType for Esp32 { fn get_flash_segments<'a>( image: &'a FirmwareImage, + partition_table: Option, ) -> Box, Error>> + 'a> { let bootloader = include_bytes!("../../bootloader/esp32-bootloader.bin"); - let partition_table = PartitionTable::basic( - NVS_ADDR, - NVS_SIZE, - PHY_INIT_DATA_ADDR, - PHY_INIT_DATA_SIZE, - APP_ADDR, - APP_SIZE, - ) - .to_bytes(); + let partition_table = if let Some(table) = partition_table { + table + } else { + PartitionTable::basic( + NVS_ADDR, + NVS_SIZE, + PHY_INIT_DATA_ADDR, + PHY_INIT_DATA_SIZE, + APP_ADDR, + APP_SIZE, + ) + }; + let partition_table = partition_table.to_bytes(); fn get_data<'a>(image: &'a FirmwareImage) -> Result, Error> { let mut data = Vec::new(); @@ -177,7 +181,7 @@ fn test_esp32_rom() { let image = FirmwareImage::from_data(&input_bytes).unwrap(); - let segments = Esp32::get_flash_segments(&image) + let segments = Esp32::get_flash_segments(&image, None) .collect::, Error>>() .unwrap(); diff --git a/espflash/src/chip/esp32c3.rs b/espflash/src/chip/esp32c3.rs index f261f305..0f5c18fa 100644 --- a/espflash/src/chip/esp32c3.rs +++ b/espflash/src/chip/esp32c3.rs @@ -8,8 +8,7 @@ use crate::{ WP_PIN_DISABLED, }, elf::{FirmwareImage, RomSegment, ESP_CHECKSUM_MAGIC}, - partition_table::PartitionTable, - Error, + Error, PartitionTable, }; use std::{borrow::Cow, io::Write, iter::once}; @@ -53,18 +52,23 @@ impl ChipType for Esp32c3 { fn get_flash_segments<'a>( image: &'a FirmwareImage, + partition_table: Option, ) -> Box, Error>> + 'a> { let bootloader = include_bytes!("../../bootloader/esp32c3-bootloader.bin"); - let partition_table = PartitionTable::basic( - NVS_ADDR, - NVS_SIZE, - PHY_INIT_DATA_ADDR, - PHY_INIT_DATA_SIZE, - APP_ADDR, - APP_SIZE, - ) - .to_bytes(); + let partition_table = if let Some(table) = partition_table { + table + } else { + PartitionTable::basic( + NVS_ADDR, + NVS_SIZE, + PHY_INIT_DATA_ADDR, + PHY_INIT_DATA_SIZE, + APP_ADDR, + APP_SIZE, + ) + }; + let partition_table = partition_table.to_bytes(); fn get_data<'a>(image: &'a FirmwareImage) -> Result, Error> { let mut data = Vec::new(); diff --git a/espflash/src/chip/esp8266.rs b/espflash/src/chip/esp8266.rs index cd74e1f8..1e011aa6 100644 --- a/espflash/src/chip/esp8266.rs +++ b/espflash/src/chip/esp8266.rs @@ -5,7 +5,7 @@ use crate::{ chip::{Chip, SpiRegisters}, elf::{update_checksum, CodeSegment, FirmwareImage, RomSegment, ESP_CHECKSUM_MAGIC}, flasher::FlashSize, - Error, + Error, PartitionTable, }; use std::{borrow::Cow, io::Write, iter::once, mem::size_of}; @@ -34,6 +34,7 @@ impl ChipType for Esp8266 { fn get_flash_segments<'a>( image: &'a FirmwareImage, + _partition_table: Option, ) -> Box, Error>> + 'a> { // irom goes into a separate plain bin let irom_data = merge_rom_segments(image.rom_segments(Chip::Esp8266)) @@ -143,7 +144,7 @@ fn test_esp8266_rom() { let image = FirmwareImage::from_data(&input_bytes).unwrap(); - let segments = Esp8266::get_flash_segments(&image) + let segments = Esp8266::get_flash_segments(&image, None) .collect::, Error>>() .unwrap(); diff --git a/espflash/src/chip/mod.rs b/espflash/src/chip/mod.rs index 9c9af0f1..b0333834 100644 --- a/espflash/src/chip/mod.rs +++ b/espflash/src/chip/mod.rs @@ -4,7 +4,7 @@ use strum_macros::Display; use crate::{ elf::{update_checksum, CodeSegment, FirmwareImage, RomSegment}, flasher::FlashSize, - Error, + Error, PartitionTable, }; use std::{io::Write, str::FromStr}; @@ -29,6 +29,7 @@ pub trait ChipType { /// Get the firmware segments for writing an image to flash fn get_flash_segments<'a>( image: &'a FirmwareImage, + partition_table: Option, ) -> Box, Error>> + 'a>; fn addr_is_flash(addr: u32) -> bool; @@ -112,11 +113,12 @@ impl Chip { pub fn get_flash_segments<'a>( &self, image: &'a FirmwareImage, + partition_table: Option, ) -> Box, Error>> + 'a> { match self { - Chip::Esp32 => Esp32::get_flash_segments(image), - Chip::Esp32c3 => Esp32c3::get_flash_segments(image), - Chip::Esp8266 => Esp8266::get_flash_segments(image), + Chip::Esp32 => Esp32::get_flash_segments(image, partition_table), + Chip::Esp32c3 => Esp32c3::get_flash_segments(image, partition_table), + Chip::Esp8266 => Esp8266::get_flash_segments(image, None), } } diff --git a/espflash/src/flasher.rs b/espflash/src/flasher.rs index 4e6259cf..064d10c7 100644 --- a/espflash/src/flasher.rs +++ b/espflash/src/flasher.rs @@ -7,7 +7,7 @@ use std::{mem::size_of, thread::sleep}; use crate::{ chip::Chip, connection::Connection, elf::FirmwareImage, encoder::SlipEncoder, error::RomError, - Error, + Error, PartitionTable, }; type Encoder<'a> = SlipEncoder<'a, Box>; @@ -551,12 +551,16 @@ impl Flasher { } /// Load an elf image to flash and execute it - pub fn load_elf_to_flash(&mut self, elf_data: &[u8]) -> Result<(), Error> { + pub fn load_elf_to_flash( + &mut self, + elf_data: &[u8], + partition_table: Option, + ) -> Result<(), Error> { self.enable_flash(self.spi_params)?; let mut image = FirmwareImage::from_data(elf_data).map_err(|_| Error::InvalidElf)?; image.flash_size = self.flash_size(); - for segment in self.chip.get_flash_segments(&image) { + for segment in self.chip.get_flash_segments(&image, partition_table) { let segment = segment?; let addr = segment.addr; let block_count = (segment.data.len() + FLASH_WRITE_SIZE - 1) / FLASH_WRITE_SIZE; diff --git a/espflash/src/lib.rs b/espflash/src/lib.rs index 17b97ca0..295ff78f 100644 --- a/espflash/src/lib.rs +++ b/espflash/src/lib.rs @@ -11,3 +11,4 @@ pub use chip::Chip; pub use config::Config; pub use error::Error; pub use flasher::Flasher; +pub use partition_table::PartitionTable; diff --git a/espflash/src/main.rs b/espflash/src/main.rs index 7724b55e..b97f781f 100644 --- a/espflash/src/main.rs +++ b/espflash/src/main.rs @@ -62,7 +62,7 @@ fn main() -> Result<()> { if ram { flasher.load_elf_to_ram(&input_bytes)?; } else { - flasher.load_elf_to_flash(&input_bytes)?; + flasher.load_elf_to_flash(&input_bytes, None)?; } Ok(()) diff --git a/espflash/src/partition_table.rs b/espflash/src/partition_table.rs index 71ee882d..4ac92884 100644 --- a/espflash/src/partition_table.rs +++ b/espflash/src/partition_table.rs @@ -1,58 +1,94 @@ -use std::io::Write; - use md5::{Context, Digest}; +use regex::Regex; +use serde::{Deserialize, Deserializer}; + +use std::{error::Error, io::Write}; const MAX_PARTITION_LENGTH: usize = 0xC00; const PARTITION_TABLE_SIZE: usize = 0x1000; +const MAX_PARTITION_TABLE_ENTRIES: usize = 95; -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Deserialize)] #[repr(u8)] #[allow(dead_code)] pub enum Type { + #[serde(alias = "app")] App = 0x00, + #[serde(alias = "data")] Data = 0x01, } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Deserialize)] #[repr(u8)] #[allow(dead_code)] pub enum AppType { + #[serde(alias = "factory")] Factory = 0x00, + #[serde(alias = "ota_0")] Ota0 = 0x10, + #[serde(alias = "ota_1")] Ota1 = 0x11, + #[serde(alias = "ota_2")] Ota2 = 0x12, + #[serde(alias = "ota_3")] Ota3 = 0x13, + #[serde(alias = "ota_4")] Ota4 = 0x14, + #[serde(alias = "ota_5")] Ota5 = 0x15, + #[serde(alias = "ota_6")] Ota6 = 0x16, + #[serde(alias = "ota_7")] Ota7 = 0x17, + #[serde(alias = "ota_8")] Ota8 = 0x18, + #[serde(alias = "ota_9")] Ota9 = 0x19, + #[serde(alias = "ota_10")] Ota10 = 0x1a, + #[serde(alias = "ota_11")] Ota11 = 0x1b, + #[serde(alias = "ota_12")] Ota12 = 0x1c, + #[serde(alias = "ota_13")] Ota13 = 0x1d, + #[serde(alias = "ota_14")] Ota14 = 0x1e, + #[serde(alias = "ota_15")] Ota15 = 0x1f, + #[serde(alias = "test")] Test = 0x20, } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Deserialize)] #[repr(u8)] #[allow(dead_code)] pub enum DataType { + #[serde(alias = "ota")] Ota = 0x00, + #[serde(alias = "phy")] Phy = 0x01, + #[serde(alias = "nvs")] Nvs = 0x02, + #[serde(alias = "coredump")] CoreDump = 0x03, + #[serde(alias = "nvs_keys")] NvsKeys = 0x04, + #[serde(alias = "efuse")] EFuse = 0x05, + #[serde(alias = "undefined")] + Undefined = 0x06, + #[serde(alias = "esphttpd")] EspHttpd = 0x80, + #[serde(alias = "fat")] Fat = 0x81, + #[serde(alias = "spiffs")] Spiffs = 0x82, } +#[derive(Debug, Deserialize)] #[allow(dead_code)] +#[serde(untagged)] pub enum SubType { App(AppType), Data(DataType), @@ -67,12 +103,14 @@ impl SubType { } } +#[derive(Debug)] pub struct PartitionTable { partitions: Vec, } impl PartitionTable { - /// Create a basic partition table with NVS, PHY init data, and the app partition + /// Create a basic partition table with NVS, PHY init data, and the app + /// partition pub fn basic( nvs_offset: u32, nvs_size: u32, @@ -88,26 +126,46 @@ impl PartitionTable { SubType::Data(DataType::Nvs), nvs_offset, nvs_size, - 0, + None, ), Partition::new( String::from("phy_init"), SubType::Data(DataType::Phy), phy_init_data_offset, phy_init_data_size, - 0, + None, ), Partition::new( String::from("factory"), SubType::App(AppType::Factory), app_offset, app_size, - 0, + None, ), ], } } + /// Attempt to parse a partition table from the given string. For more + /// information on the paritition table CSV format see: + /// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/partition-tables.html + pub fn try_from_str>(data: S) -> Result> { + let data = data.into(); + let mut reader = csv::ReaderBuilder::new() + .comment(Some(b'#')) + .has_headers(false) + .trim(csv::Trim::All) + .from_reader(data.trim().as_bytes()); + + let mut partitions = Vec::with_capacity(MAX_PARTITION_TABLE_ENTRIES); + for partition in reader.deserialize() { + let partition: Partition = partition?; + partitions.push(partition); + } + + Ok(Self { partitions }) + } + pub fn to_bytes(&self) -> Vec { let mut result = Vec::with_capacity(PARTITION_TABLE_SIZE); self.save(&mut result).unwrap(); @@ -139,17 +197,27 @@ impl PartitionTable { const PARTITION_SIZE: usize = 32; +#[derive(Debug, Deserialize)] struct Partition { + #[serde(deserialize_with = "deserialize_partition_name")] name: String, ty: Type, sub_type: SubType, + #[serde(deserialize_with = "deserialize_partition_offset_or_size")] offset: u32, + #[serde(deserialize_with = "deserialize_partition_offset_or_size")] size: u32, - flags: u32, + flags: Option, } impl Partition { - pub fn new(name: String, sub_type: SubType, offset: u32, size: u32, flags: u32) -> Self { + pub fn new( + name: String, + sub_type: SubType, + offset: u32, + size: u32, + flags: Option, + ) -> Self { Partition { name, ty: match sub_type { @@ -174,12 +242,71 @@ impl Partition { *dest = source; } writer.write_all(&name_bytes)?; - writer.write_all(&self.flags.to_le_bytes())?; + + let flags = match &self.flags { + Some(f) => f.to_le_bytes(), + None => 0u32.to_le_bytes(), + }; + writer.write_all(&flags)?; Ok(()) } } +fn deserialize_partition_name<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + // Partition names longer than 16 characters are truncated. + // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/partition-tables.html#name-field + const MAX_LENGTH: usize = 16; + + let buf = String::deserialize(deserializer)?; + let maybe_truncated = match buf.as_str().char_indices().nth(MAX_LENGTH) { + Some((idx, _)) => String::from(&buf[..idx]), + None => buf, + }; + + Ok(maybe_truncated) +} + +fn deserialize_partition_offset_or_size<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + use serde::de::Error; + + let buf = String::deserialize(deserializer)?; + let re = Regex::new(r"(?i)^(\d+)([km]{1})$").unwrap(); + + // NOTE: Partitions of type 'app' must be placed at offsets aligned to 0x10000 + // (64K). + // TODO: The specification states that offsets may be left blank, however that + // is not presently supported in this implementation. + if buf.starts_with("0x") { + // Hexadecimal format + let src = buf.trim_start_matches("0x"); + let size = u32::from_str_radix(src, 16).unwrap(); + + Ok(size) + } else if let Ok(size) = buf.parse::() { + // Decimal format + Ok(size) + } else if let Some(captures) = re.captures(&buf) { + // Size multiplier format (1k, 2M, etc.) + let digits = captures.get(1).unwrap().as_str().parse::().unwrap(); + let multiplier = match captures.get(2).unwrap().as_str() { + "k" | "K" => 1024, + "m" | "M" => 1024 * 1024, + _ => unreachable!(), + }; + + Ok(digits * multiplier) + } else { + Err(Error::custom("invalid partition size/offset format")) + } +} + struct HashWriter { inner: W, hasher: Context, @@ -209,29 +336,62 @@ impl HashWriter { } } -#[test] -fn test_basic() { - use std::fs::read; - const NVS_ADDR: u32 = 0x9000; - const PHY_INIT_DATA_ADDR: u32 = 0xf000; - const APP_ADDR: u32 = 0x10000; - - const NVS_SIZE: u32 = 0x6000; - const PHY_INIT_DATA_SIZE: u32 = 0x1000; - const APP_SIZE: u32 = 0x3f0000; - - let expected = read("./tests/data/partitions.bin").unwrap(); - let table = PartitionTable::basic( - NVS_ADDR, - NVS_SIZE, - PHY_INIT_DATA_ADDR, - PHY_INIT_DATA_SIZE, - APP_ADDR, - APP_SIZE, - ); - - let result = table.to_bytes(); - - assert_eq!(expected.len(), result.len()); - assert_eq!(expected, result.as_slice()); +#[cfg(test)] +mod tests { + use super::*; + + const PTABLE_0: &str = " +# ESP-IDF Partition Table +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 1M, +"; + + const PTABLE_1: &str = " +# ESP-IDF Partition Table +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x4000, +otadata, data, ota, 0xd000, 0x2000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 1M, +ota_0, app, ota_0, 0x110000, 1M, +ota_1, app, ota_1, 0x210000, 1M, +"; + + #[test] + fn test_basic() { + use std::fs::read; + const NVS_ADDR: u32 = 0x9000; + const PHY_INIT_DATA_ADDR: u32 = 0xf000; + const APP_ADDR: u32 = 0x10000; + + const NVS_SIZE: u32 = 0x6000; + const PHY_INIT_DATA_SIZE: u32 = 0x1000; + const APP_SIZE: u32 = 0x3f0000; + + let expected = read("./tests/data/partitions.bin").unwrap(); + let table = PartitionTable::basic( + NVS_ADDR, + NVS_SIZE, + PHY_INIT_DATA_ADDR, + PHY_INIT_DATA_SIZE, + APP_ADDR, + APP_SIZE, + ); + + let result = table.to_bytes(); + + assert_eq!(expected.len(), result.len()); + assert_eq!(expected, result.as_slice()); + } + + #[test] + fn test_from_str() { + let pt0 = PartitionTable::try_from_str(PTABLE_0); + assert!(pt0.is_ok()); + + let pt1 = PartitionTable::try_from_str(PTABLE_1); + assert!(pt1.is_ok()); + } }