5.Rust嵌入式入门——OTA
本节将学习如何利用网络来获取更新文件,并通过使用
esp-idf-svc库中的OTA(Over-the-Air)框架来实现ESP32-C3设备的远程固件升级功能。
OTA
OTA(Over-the-Air)空中升级技术是指通过无线通信的方式对微控制器(MCU)的软件进行远程管理和更新的方法。这项技术使得MCU可以在部署后无需物理接触就能接收和安装软件更新,从而显著提升了设备的可维护性和灵活性。
特点
- 远程更新:设备可以通过无线网络接收软件更新,无需人工介入。
- 软件升级:可以更新固件,修正软件缺陷,添加新功能或优化现有功能。
- 成本节约:减少了现场维护所需的物流和人力成本。
应用场景
- 消费电子产品:如智能家居设备中的MCU。
- 工业自动化:用于工厂自动化设备中的MCU。
- 物联网设备:包括传感器节点和其他嵌入式系统。
实现流程
- 准备更新包:开发者构建新的固件版本,并打包成适合无线传输的格式。
- 上传至服务器:更新包被上传到OTA服务器。
- 设备连接网络:MCU设备通过Wi-Fi或蓝牙等方式连接到互联网。
- 检查更新:设备定期检查是否有可用的更新。
- 下载新固件:一旦检测到新版本,设备就会从服务器下载新固件。
- 验证与安装:下载完成后,设备验证固件的完整性和正确性,然后安装到备用分区。
- 重启与验证:设备重启后从新固件分区启动,并进行最终的验证。
实现
1.配置分区文件
在项目目录下新建partitions.csv,然后将下面内容复制进去
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 2M,
app1, app, ota_1, ,0x1f0000,
修改.cargo/config.toml
在runner后面指定分区表
[build]
target = "riscv32imc-esp-espidf"
[target.riscv32imc-esp-espidf]
linker = "ldproxy"
# 在后面指定分区表
runner = "espflash flash --monitor --partition-table=partitions.csv"
# ...
2.制作镜像文件
将上一节的蓝牙服务制作为进行
# 编译
cargo build --release --bin ble_server
# 制作镜像
espflash save-image --chip esp32c3 target/riscv32imc-esp-espidf/release/ble_server ble_server.bin
然后可以在项目目录下看到文件ble_server.bin。
3.安装插件和查看本机IP
推荐安装Live Server VsCode扩展,能快速启动一个本地文件服务。
然后点击Go Live就能在端口5500开启一个文件服务。
获取wifi网卡在局域网的IP地址
ipconfig
访问http://192.168.88.235:5500出现下面页面表示成功。
4.编写OTA服务
新建文件src/bin/ota.rs
use std::sync::{ mpsc::{ channel, Sender }, Arc, Condvar, Mutex };
use anyhow::anyhow;
use embedded_svc::http::{ client::Client, Headers };
use esp32_nimble::{ utilities::BleUuid, BLEAdvertisementData, NimbleProperties };
use esp_idf_svc::{
http::{ client::{ Configuration, EspHttpConnection }, Method },
ota::{ EspOta, FirmwareInfo },
};
// 配置结构体,用于读取配置文件
#[toml_cfg::toml_config]
#[derive(Debug)]
pub struct Config {
// 默认SSID和密码
#[default("Wokwi-GUEST")]
wifi_ssid: &'static str,
#[default("")]
wifi_psk: &'static str,
}
// 常量定义,用于固件下载的chunk大小、最大和最小尺寸
const FIRMWARE_DOWNLOAD_CHUNK_SIZE: usize = 1024 * 20;
const FIRMWARE_MAX_SIZE: usize = 1024 * 1024;
const FIRMWARE_MIN_SIZE: usize = size_of::<FirmwareInfo>() + 1024;
// 主函数,程序的入口点
fn main() -> anyhow::Result<()> {
// 初始化系统循环、外设和NVS闪存
let (sysloop, peripherals, nvs) = rust_embedded_study::init()?;
// 连接WiFi
let _wifi = rust_embedded_study::wifi::connect_wifi(
CONFIG.wifi_ssid,
&CONFIG.wifi_psk,
peripherals.modem,
sysloop,
nvs
)?;
// 初始化BLE设备和广告
let device = esp32_nimble::BLEDevice::take();
let advertising = device.get_advertising();
// 获取BLE服务器
let server = device.get_server();
// 配置BLE连接时的回调函数
server.on_connect(|server, desc| {
log::info!("on_connect: {:#?}", desc);
server.update_conn_params(desc.conn_handle(), 24, 48, 0, 60).unwrap();
if server.connected_count() < (esp_idf_svc::sys::CONFIG_BT_NIMBLE_MAX_CONNECTIONS as _) {
advertising.lock().start().unwrap();
}
});
// 配置BLE断开连接时的回调函数
server.on_disconnect(|desc, reason| {
log::warn!("on_disconnect: {:#?}, reason: {:#?}", desc, reason)
});
// 创建BLE服务和OTA特性
let services = server.create_service(BleUuid::from_uuid16(0x8849));
let ota_characteristic = services
.lock()
.create_characteristic(
BleUuid::from_uuid16(0xffa1),
NimbleProperties::WRITE | NimbleProperties::NOTIFY
);
// 创建消息通道
let (tx, rx) = channel::<(usize, usize)>();
// 创建共享状态
let state = Arc::new((Mutex::new(false), Condvar::new()));
// 克隆状态,用于OTA写入时的通知
let state_clone = state.clone();
// 配置OTA特性的写入回调
ota_characteristic
.lock()
.on_write(move |args| {
let data = args.recv_data();
if data[0] == 1 {
let mut is_start = state_clone.0.lock().unwrap();
*is_start = true;
state_clone.1.notify_all();
}
if data[0] == 2 {
unsafe {
esp_idf_svc::sys::esp_restart();
}
}
})
.create_2904_descriptor();
// 创建线程进行OTA更新
std::thread::spawn(move || {
let (lock, condvar) = &*state;
let mut is_start = lock.lock().unwrap();
while !*is_start {
is_start = condvar.wait(is_start).unwrap();
}
log::warn!("Start OTA");
// 注意将其中的URL替换成您的本地IP地址。
firmware("http://192.168.88.235:5500/ble_server.bin", tx)
});
// 配置广告数据并启动广告
advertising
.lock()
.set_data(
BLEAdvertisementData::new()
.name("ESP32_OTA")
.add_service_uuid(BleUuid::from_uuid16(0x8849))
)?;
advertising.lock().start()?;
// 打印蓝牙服务相关日志
server.ble_gatts_show_local();
// 接收并处理下载进度
while let Ok((read_len, file_size)) = rx.recv() {
let percent = (read_len * 100) / file_size;
ota_characteristic
.lock()
.set_value(&[percent as u8])
.notify();
}
Ok(())
}
// 固件下载和更新函数
fn firmware(uri: &str, tx: Sender<(usize, usize)>) -> anyhow::Result<()> {
// 初始化HTTP客户端
let mut client = Client::wrap(
EspHttpConnection::new(
&(Configuration {
buffer_size: Some(1024 * 4),
..Default::default()
})
)?
);
// 准备下载请求
let request = client.request(Method::Get, uri, &[("Accept", "application/octet-stream")])?;
let mut response = request.submit()?;
// 检查响应状态
if response.status() != 200 {
return Err(anyhow!("Firmware download failed: {}", response.status()));
}
// 获取文件大小并进行校验
let file_size = response.content_len().unwrap_or(0) as usize;
if file_size <= FIRMWARE_MIN_SIZE {
return Err(
anyhow!(
"File size is {file_size}, too small to be a firmware! No need to proceed further."
)
);
}
if file_size > FIRMWARE_MAX_SIZE {
return Err(anyhow!("File is too big ({file_size} bytes)."));
}
log::warn!("file size: {file_size}");
// 初始化OTA更新
let mut ota = EspOta::new()?;
let mut buff = vec![0;FIRMWARE_DOWNLOAD_CHUNK_SIZE];
let mut total_read_len = 0usize;
// 开始OTA更新
let mut work = ota.initiate_update()?;
// 循环读取和写入数据
let dl_result = loop {
let n = response.read(&mut buff)?;
total_read_len += n;
tx.send((total_read_len, file_size))?;
if n > 0 {
if let Err(e) = work.write(&buff[..n]) {
log::error!("Failed to write to OTA. {e}");
break Err(anyhow!(e));
}
}
if total_read_len >= file_size {
break Ok(());
}
};
// 检查下载结果并相应处理
if dl_result.is_err() {
return Ok(work.abort()?);
}
if total_read_len < file_size {
log::error!(
"Supposed to download {file_size} bytes, but we could only get {total_read_len}. May be network error?"
);
return Ok(work.abort()?);
}
// 完成OTA更新
work.complete()?;
log::info!("OTA done!");
Ok(())
}
主要功能:
-
初始化系统环境:
- 初始化系统循环、外设和 NVS 闪存。
-
连接 WiFi:
- 根据配置文件中的 SSID 和密码连接到 WiFi 网络。
-
配置 BLE (Bluetooth Low Energy):
- 设置 BLE 广告数据和服务。
- 配置 BLE 服务器上的连接和断开连接事件处理。
- 创建一个 OTA 特性(characteristic),用于接收 OTA 更新的启动信号和重启指令。
-
OTA 更新逻辑:
- 创建一个线程来等待 OTA 更新的启动信号。
- 从指定的 HTTP 服务器下载新的固件文件。
- 验证固件文件的大小。
- 向 OTA 特性发送下载进度通知。
- 将固件数据写入 OTA 更新工作区。
- 如果下载成功,则完成 OTA 更新;如果失败,则中止更新。
-
主循环:
- 启动 BLE 广告。
- 监听 OTA 特性上的写入事件。
- 在接收到启动信号后,开始 OTA 更新过程。
- 在接收到重启指令后,重启设备。
详细说明
-
BLE OTA 特性:BLE 服务中创建了一个 OTA 特性,当该特性接收到值
1时,会通知主线程开始 OTA 更新;当接收到值2时,设备会重启。 -
OTA 更新线程:在主线程中创建了一个子线程,该线程等待来自 OTA 特性的启动信号。一旦接收到信号,它会调用
firmware函数开始下载固件。 -
固件下载:
firmware函数使用 ESP32 的 HTTP 客户端库来下载固件文件。下载过程中,它会验证固件文件的大小是否在允许范围内,并通过 BLE 特性发送进度给外部设备。 -
进度通知:下载过程中,通过 BLE 特性向外部设备发送百分比进度。
总结
通过上述代码,我们成功实现了 ESP32C3 微控制器的 OTA 更新功能。这一实现整合了 WiFi 连接、BLE 通信和 HTTP 客户端,使固件能够远程下载和更新。这样的设计让外部设备(例如智能手机)能够通过 BLE 控制固件的下载和更新流程。
5.写入程序
擦除flash
espflash erase-flash
将程序写入到esp32c3中
cargo run --bin ota
6.使用BLE调试助手进行调试
最后
🎉恭喜您成功实现了OTA功能。您所探索的代码可以在GitHub仓库yexiyue/rust-embedded-study中找到。
如果您觉得不错,请点个关注或者收藏,感谢您的支持。