Simple ota example (#3629)

* Fix esp-bootloader-esp-idf

* Use OTA enabled partition table for examples

* Add simple OTA example

* CHANGELOG.md

* Create a dummy `ota_image` in CI

* mkdir

* Remove unnecessary details from CHANGELOG

* Make non-Window's users life easier

* Test ROM function in esp-bootloader-esp-idf

* Fix
This commit is contained in:
Björn Quentin 2025-06-13 15:42:09 +02:00 committed by GitHub
parent c15fc6773e
commit 45248100f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 207 additions and 3 deletions

View File

@ -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

View File

@ -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::<i64>().unwrap(), 0).unwrap(),
Err(_) => Utc::now(),

View File

@ -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;

View File

@ -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) }
}
}

View File

@ -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",

8
examples/partitions.csv Normal file
View File

@ -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,
1 # ESP-IDF Partition Table
2 # Name, Type, SubType, Offset, Size, Flags
3 nvs, data, nvs, 0x9000,0x4000,
4 otadata, data, ota, 0xd000,0x2000,
5 phy_init, data, phy, 0xf000,0x1000,
6 factory, app, factory,0x10000,0x100000,
7 ota_0, app, ota_0, 0x110000,0x100000,
8 ota_1, app, ota_1, 0x210000,0x100000,

View File

@ -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();
}
}
}

View File

@ -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",

View File

@ -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"

20
hil-test/tests/otadata.rs Normal file
View File

@ -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);
}
}

View File

@ -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 {