diff --git a/esp-bootloader-esp-idf/CHANGELOG.md b/esp-bootloader-esp-idf/CHANGELOG.md index 9f702273a..6c6b5daba 100644 --- a/esp-bootloader-esp-idf/CHANGELOG.md +++ b/esp-bootloader-esp-idf/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fixed a problem with calculating the otadata checksum (#3629) ### Removed diff --git a/esp-bootloader-esp-idf/build.rs b/esp-bootloader-esp-idf/build.rs index 2032bf869..d5998e1a1 100644 --- a/esp-bootloader-esp-idf/build.rs +++ b/esp-bootloader-esp-idf/build.rs @@ -4,6 +4,8 @@ use chrono::{TimeZone, Utc}; use esp_config::{ConfigOption, Validator, generate_config}; fn main() { + println!("cargo::rustc-check-cfg=cfg(embedded_test)"); + let build_time = match env::var("SOURCE_DATE_EPOCH") { Ok(val) => Utc.timestamp_opt(val.parse::().unwrap(), 0).unwrap(), Err(_) => Utc::now(), diff --git a/esp-bootloader-esp-idf/src/lib.rs b/esp-bootloader-esp-idf/src/lib.rs index 5b2c3ee12..6da36d7d2 100644 --- a/esp-bootloader-esp-idf/src/lib.rs +++ b/esp-bootloader-esp-idf/src/lib.rs @@ -33,6 +33,8 @@ pub(crate) use rom as crypto; #[cfg(feature = "std")] mod non_rom; +#[cfg(embedded_test)] +pub use crypto::Crc32 as Crc32ForTesting; #[cfg(feature = "std")] pub(crate) use non_rom as crypto; diff --git a/esp-bootloader-esp-idf/src/rom.rs b/esp-bootloader-esp-idf/src/rom.rs index 3f16192ad..fa845c946 100644 --- a/esp-bootloader-esp-idf/src/rom.rs +++ b/esp-bootloader-esp-idf/src/rom.rs @@ -10,6 +10,6 @@ impl Crc32 { fn esp_rom_crc32_le(crc: u32, buf: *const u8, len: u32) -> u32; } - unsafe { esp_rom_crc32_le(0, data.as_ptr(), data.len() as u32) } + unsafe { esp_rom_crc32_le(u32::MAX, data.as_ptr(), data.len() as u32) } } } diff --git a/examples/.cargo/config.toml b/examples/.cargo/config.toml index 7fcbcbfad..5c7bc9735 100644 --- a/examples/.cargo/config.toml +++ b/examples/.cargo/config.toml @@ -8,14 +8,14 @@ esp32s2 = "run --release --features=esp32s2 --target=xtensa-esp32s2-none-elf" esp32s3 = "run --release --features=esp32s3 --target=xtensa-esp32s3-none-elf" [target.'cfg(target_arch = "riscv32")'] -runner = "espflash flash --monitor" +runner = "espflash flash --monitor --partition-table=partitions.csv" rustflags = [ "-C", "link-arg=-Tlinkall.x", "-C", "force-frame-pointers", ] [target.'cfg(target_arch = "xtensa")'] -runner = "espflash flash --monitor" +runner = "espflash flash --monitor --partition-table=partitions.csv" rustflags = [ # GNU LD "-C", "link-arg=-Wl,-Tlinkall.x", diff --git a/examples/partitions.csv b/examples/partitions.csv new file mode 100644 index 000000000..86f81dae2 --- /dev/null +++ b/examples/partitions.csv @@ -0,0 +1,8 @@ +# 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,0x100000, +ota_0, app, ota_0, 0x110000,0x100000, +ota_1, app, ota_1, 0x210000,0x100000, diff --git a/examples/src/bin/ota_update.rs b/examples/src/bin/ota_update.rs new file mode 100644 index 000000000..cbbb7728e --- /dev/null +++ b/examples/src/bin/ota_update.rs @@ -0,0 +1,158 @@ +//! OTA Update Example +//! +//! This shows the basics of dealing with partitions and changing the active partition. +//! For simplicity it will flash an application image embedded into the binary. +//! In a real world application you can get the image via HTTP(S), UART or from an sd-card etc. +//! +//! Adjust the target and the chip in the following commands according to the chip used! +//! +//! - `cargo xtask build examples examples esp32 --example=gpio_interrupt` +//! - `espflash save-image --chip=esp32 examples/target/xtensa-esp32-none-elf/release/gpio_interrupt examples/target/ota_image` +//! - `cargo xtask build examples examples esp32 --example=ota_update` +//! - `espflash save-image --chip=esp32 examples/target/xtensa-esp32-none-elf/release/ota_update examples/target/ota_image` +//! - erase whole flash via `espflash erase-flash` (this is to make sure otadata is cleared and no code is flashed to any partition) +//! - run via `cargo xtask run example examples esp32 --example=ota_update` +//! +//! On first boot notice the firmware partition gets booted ("Loaded app from partition at offset 0x10000"). +//! Press the BOOT button, once finished press the RESET button. +//! +//! Notice OTA0 gets booted ("Loaded app from partition at offset 0x110000"). +//! +//! Once again press BOOT, when finished press RESET. +//! You will see the `gpio_interrupt` example gets booted from OTA1 ("Loaded app from partition at offset 0x210000") +//! +//! See https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/ota.html + +//% FEATURES: esp-storage esp-hal/unstable +//% CHIPS: esp32 esp32c2 esp32c3 esp32c6 esp32h2 esp32s2 esp32s3 + +#![no_std] +#![no_main] + +use embedded_storage::Storage; +use esp_backtrace as _; +use esp_bootloader_esp_idf::{ + ota::Slot, + partitions::{self, AppPartitionSubType, DataPartitionSubType}, +}; +use esp_hal::{ + gpio::{Input, InputConfig, Pull}, + main, +}; +use esp_println::println; + +esp_bootloader_esp_idf::esp_app_desc!(); + +static OTA_IMAGE: &[u8] = include_bytes!("../../target/ota_image"); + +#[main] +fn main() -> ! { + let peripherals = esp_hal::init(esp_hal::Config::default()); + + let mut storage = esp_storage::FlashStorage::new(); + + let mut buffer = [0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN]; + let pt = esp_bootloader_esp_idf::partitions::read_partition_table(&mut storage, &mut buffer) + .unwrap(); + + // List all partitions - this is just FYI + for i in 0..pt.len() { + println!("{:?}", pt.get_partition(i)); + } + + // Find the OTA-data partition and show the currently active partition + let ota_part = pt + .find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data( + DataPartitionSubType::Ota, + )) + .unwrap() + .unwrap(); + let mut ota_part = ota_part.as_embedded_storage(&mut storage); + println!("Found ota data"); + + let mut ota = esp_bootloader_esp_idf::ota::Ota::new(&mut ota_part).unwrap(); + let current = ota.current_slot().unwrap(); + println!( + "current image state {:?} (only relevant if the bootloader was built with auto-rollback support)", + ota.current_ota_state() + ); + println!("current {:?} - next {:?}", current, current.next()); + + // Mark the current slot as VALID - this is only needed if the bootloader was built with auto-rollback support. + // The default pre-compiled bootloader in espflash is NOT. + if ota.current_slot().unwrap() != Slot::None + && (ota.current_ota_state().unwrap() == esp_bootloader_esp_idf::ota::OtaImageState::New + || ota.current_ota_state().unwrap() + == esp_bootloader_esp_idf::ota::OtaImageState::PendingVerify) + { + println!("Changed state to VALID"); + ota.set_current_ota_state(esp_bootloader_esp_idf::ota::OtaImageState::Valid) + .unwrap(); + } + + cfg_if::cfg_if! { + if #[cfg(any(feature = "esp32", feature = "esp32s2", feature = "esp32s3"))] { + let button = peripherals.GPIO0; + } else { + let button = peripherals.GPIO9; + } + } + + let boot_button = Input::new(button, InputConfig::default().with_pull(Pull::Up)); + + println!("Press boot button to flash and switch to the next OTA slot"); + let mut done = false; + loop { + if boot_button.is_low() && !done { + done = true; + + let next_slot = current.next(); + + println!("Flashing image to {:?}", next_slot); + + // find the target app partition + let next_app_partition = match next_slot { + Slot::None => { + // None is FACTORY if present, OTA0 otherwise + pt.find_partition(partitions::PartitionType::App(AppPartitionSubType::Factory)) + .or_else(|_| { + pt.find_partition(partitions::PartitionType::App( + AppPartitionSubType::Ota0, + )) + }) + .unwrap() + } + Slot::Slot0 => pt + .find_partition(partitions::PartitionType::App(AppPartitionSubType::Ota0)) + .unwrap(), + Slot::Slot1 => pt + .find_partition(partitions::PartitionType::App(AppPartitionSubType::Ota1)) + .unwrap(), + } + .unwrap(); + println!("Found partition: {:?}", next_app_partition); + let mut next_app_partition = next_app_partition.as_embedded_storage(&mut storage); + + // write to the app partition + for (sector, chunk) in OTA_IMAGE.chunks(4096).enumerate() { + println!("Writing sector {sector}..."); + next_app_partition + .write((sector * 4096) as u32, chunk) + .unwrap(); + } + + println!("Changing OTA slot and setting the state to NEW"); + let ota_part = pt + .find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data( + DataPartitionSubType::Ota, + )) + .unwrap() + .unwrap(); + let mut ota_part = ota_part.as_embedded_storage(&mut storage); + let mut ota = esp_bootloader_esp_idf::ota::Ota::new(&mut ota_part).unwrap(); + ota.set_current_slot(next_slot).unwrap(); + ota.set_current_ota_state(esp_bootloader_esp_idf::ota::OtaImageState::New) + .unwrap(); + } + } +} diff --git a/hil-test/.cargo/config.toml b/hil-test/.cargo/config.toml index 736e90985..9ce23b1ad 100644 --- a/hil-test/.cargo/config.toml +++ b/hil-test/.cargo/config.toml @@ -1,6 +1,8 @@ [target.'cfg(target_arch = "riscv32")'] runner = "probe-rs run --preverify" rustflags = [ + "--cfg", "embedded_test", + "-C", "link-arg=-Tembedded-test.x", "-C", "link-arg=-Tdefmt.x", "-C", "link-arg=-Tlinkall.x", @@ -10,6 +12,8 @@ rustflags = [ [target.'cfg(target_arch = "xtensa")'] runner = "probe-rs run --preverify" rustflags = [ + "--cfg", "embedded_test", + "-C", "link-arg=-nostartfiles", "-C", "link-arg=-Tembedded-test.x", "-C", "link-arg=-Tdefmt.x", diff --git a/hil-test/Cargo.toml b/hil-test/Cargo.toml index 68aaff9e8..42bdc749f 100644 --- a/hil-test/Cargo.toml +++ b/hil-test/Cargo.toml @@ -219,6 +219,10 @@ name = "esp_wifi_init" harness = false required-features = ["esp-wifi", "esp-alloc"] +[[test]] +name = "otadata" +harness = false + [dependencies] allocator-api2 = { version = "0.3.0", default-features = false, features = ["alloc"] } cfg-if = "1.0.0" diff --git a/hil-test/tests/otadata.rs b/hil-test/tests/otadata.rs new file mode 100644 index 000000000..8d681f1df --- /dev/null +++ b/hil-test/tests/otadata.rs @@ -0,0 +1,20 @@ +//! Tests parts of esp-bootloader-esp-idf's otadata related functionality not +//! testable on the host + +#![no_std] +#![no_main] + +use hil_test as _; + +esp_bootloader_esp_idf::esp_app_desc!(); + +#[cfg(test)] +#[embedded_test::tests(default_timeout = 3)] +mod tests { + #[test] + fn test_crc_rom_function() { + let crc = esp_bootloader_esp_idf::Crc32ForTesting::new(); + let res = crc.crc(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); + assert_eq!(res, 436745307); + } +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 748088550..dd5c12465 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -428,6 +428,11 @@ fn run_ci_checks(workspace: &Path, args: CiArgs) -> Result<()> { // Build (examples) println!("::group::Build examples"); + + // The `ota_example` expects a file named `examples/target/ota_image` - it doesn't care about the contents however + std::fs::create_dir_all("./examples/target")?; + std::fs::write("./examples/target/ota_image", "DUMMY")?; + examples( workspace, ExamplesArgs {