mcpField
E
esp32-rust-embedded

esp32-rust-embedded

github.com/psytraxx开发3101 次安装4 周前更新
代码仓库

关于 esp32-rust-embedded

该技能为使用 ESP-RS 和 Embassy 开发 ESP32 无标准库 Rust 固件提供指导,面向嵌入式开发者。内容涵盖构建、外设、异步任务、无线连接与低功耗模式,并提示相关依赖版本变化。

主要功能

  1. 01介绍 ESP-RS 与 Embassy 依赖配置
  2. 02指导构建、烧录及常见错误排查
  3. 03提供 GPIO、ADC 与传感器模式
  4. 04说明异步任务和任务间通信
  5. 05涵盖 WiFi、显示及深度睡眠

适用场景

  1. 01搭建 ESP32 的 Rust 固件项目
  2. 02实现传感器采集与异步任务
  3. 03排查 WiFi、构建或睡眠唤醒问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/psytraxx/esp32-homecontrol-no-std-rs --skill esp32-rust-embedded

安装后在 Claude Code 中输入/esp32-rust-embedded确认已加载。

详细介绍

ESP32 嵌入式 Rust 专家

为使用 ESP-RS 生态系统和 Embassy 异步框架在 ESP32 微控制器上进行 no-std Rust 开发提供专家指导。

ESP-RS 生态系统技术栈

核心依赖项
esp-hal = { version = "~1.1.0", features = ["esp32s3", "log-04", "unstable"] }
esp-rtos = { version = "0.3.0", features = ["embassy", "esp-alloc", "esp-radio", "esp32s3", "log-04"] }
esp-radio = { version = "0.18.0", features = ["esp-alloc", "esp32s3", "log-04", "unstable", "wifi"] }
esp-bootloader-esp-idf = { version = "0.5.0", features = ["esp32s3", "log-04"] }
esp-alloc = "0.10.0"
esp-println = { version = "0.17.0", features = ["esp32s3", "log-04"] }

# smoltcp is now a DIRECT dependency (no longer a feature of esp-radio)
smoltcp = { version = "0.13.0", default-features = false, features = [
  "log", "medium-ethernet", "multicast",
  "proto-dhcpv4", "proto-dns", "proto-ipv4",
  "socket-dns", "socket-icmp", "socket-raw", "socket-tcp", "socket-udp",
] }
Embassy 框架
embassy-executor = { version = "0.10.0", features = ["log"] }
embassy-time = { version = "0.5.0", features = ["log"] }
embassy-net = { version = "0.9.1", features = ["dhcpv4", "log", "medium-ethernet", "tcp", "udp"] }
embassy-sync = { version = "0.7.2" }
依赖层级
esp-radio (WiFi) -> esp-rtos (scheduler) -> esp-hal (HAL) -> esp-phy (PHY)
embassy-executor -> embassy-time -> embassy-sync -> embassy-net
Rust 版本与 MSRV
  • 版本:2024(Cargo.toml 中的 edition = "2024")
  • MSRV:1.88(rust-version = "1.88")
  • 二进制文件位置:src/bin/main.rs(而非 src/main.rs)——Cargo.toml 中需要 [[bin]] 条目

构建与烧录

环境设置
# Install ESP toolchain (one-time)
espup install
source $HOME/export-esp.sh

# Configure credentials (.env file)
cp .env.dist .env
# Edit: WIFI_SSID, WIFI_PSK, MQTT_HOSTNAME, MQTT_USERNAME, MQTT_PASSWORD
构建命令
# Quick build and flash
./run.sh

# Manual release build (recommended)
cargo run --release

# Debug build (slower on device)
cargo run
Cargo 配置文件优化
[profile.dev]
opt-level = "s"  # Rust debug too slow for ESP32

[profile.release]
lto = 'fat'
opt-level = 's'
codegen-units = 1
常见构建错误

链接器错误:未定义符号 _stack_start

  • 检查 build.rs 是否配置了 linkall.x
  • 确认 esp-hal 版本兼容性

未定义符号:esp_rtos_*(“esp-radio 未启用调度器”)

  • 确保启动 esp-rtos 时同时提供计时器和软件中断(在 0.3.0 中变更):
let timg0 = TimerGroup::new(peripherals.TIMG0);
let sw_interrupt = esp_hal::interrupt::software::SoftwareInterruptControl::new(
    peripherals.SW_INTERRUPT
);
esp_rtos::start(timg0.timer0, sw_interrupt.software_interrupt0);

环境变量错误

  • 变量通过 env!() 宏在编译时读取
  • 变更后需要完整重新构建

No-Std 模式

应用程序入口
#![no_std]
#![no_main]
// Recommended lints (now standard in esp-generate templates)
#![deny(clippy::mem_forget)]  // esp-hal types must not be mem::forgotten
#![deny(clippy::large_stack_frames)]

use embassy_executor::Spawner;
use esp_hal::{clock::CpuClock, timer::timg::TimerGroup};

esp_bootloader_esp_idf::esp_app_desc!();

#[esp_rtos::main]
async fn main(spawner: Spawner) -> ! {
    // Initialize logger (reads RUST_LOG env var at compile time via esp-config)
    esp_println::logger::init_logger_from_env();

    // Initialize HAL with max CPU clock
    let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
    let peripherals = esp_hal::init(config);

    // Setup heap allocator using reclaimed memory (replaces dram2_uninit approach)
    esp_alloc::heap_allocator!(#[esp_hal::ram(reclaimed)] size: 73744);

    // Start RTOS scheduler — now requires BOTH a timer AND a software interrupt
    let timg0 = TimerGroup::new(peripherals.TIMG0);
    let sw_interrupt = esp_hal::interrupt::software::SoftwareInterruptControl::new(
        peripherals.SW_INTERRUPT
    );
    esp_rtos::start(timg0.timer0, sw_interrupt.software_interrupt0);

    // Spawn tasks...
    let _ = spawner;
    loop {}
}
内存管理
  • 使用 esp-alloc 进行动态分配
  • 优先使用具有编译期容量的 heapless 集合
  • 对需要 'static 生命周期的场景使用 static_cell::StaticCell
字符串处理
use alloc::string::String;      // Dynamic strings (heap)
use heapless::String;           // Bounded strings (stack)

let s: heapless::String<64> = heapless::String::new();

尽可能避免克隆。

StaticCell 模式
static CHANNEL: StaticCell<Channel<NoopRawMutex, Data, 3>> = StaticCell::new();

// In async function
let channel: &'static mut _ = CHANNEL.init(Channel::new());
let (sender, receiver) = (channel.sender(), channel.receiver());

硬件模式

GPIO 配置
use esp_hal::gpio::{Level, Output, OutputConfig, Pull, DriveMode};

// Standard output
let pin = Output::new(peripherals.GPIO2, Level::Low, OutputConfig::default());

// Open-drain for sensors like DHT11
let pin = Output::new(
    peripherals.GPIO1,
    Level::High,
    OutputConfig::default()
        .with_drive_mode(DriveMode::OpenDrain)
        .with_pull(Pull::None),
).into_flex();
使用校准功能读取 ADC
use esp_hal::analog::adc::{Adc, AdcConfig, AdcCalCurve, Attenuation};

let mut adc_config = AdcConfig::new();
let pin = adc_config.enable_pin_with_cal::<_, AdcCalCurve<ADC2>>(
    peripherals.GPIO11,
    Attenuation::_11dB  // 0-3.3V range
);
let adc = Adc::new(peripherals.ADC2, adc_config);

// Read with nb::block!
let value = nb::block!(adc.read_oneshot(&mut pin))?;
外设捆绑模式
pub struct SensorPeripherals {
    pub dht11_pin: GPIO1<'static>,
    pub moisture_pin: GPIO11<'static>,
    pub power_pin: GPIO16<'static>,
    pub adc2: ADC2<'static>,
}

异步任务架构

任务定义
#[embassy_executor::task]
pub async fn my_task(sender: Sender<'static, NoopRawMutex, Data, 3>) {
    loop {
        // Do work
        sender.send(data).await;
        Timer::after(Duration::from_secs(5)).await;
    }
}
任务生成
spawner.spawn(sensor_task(sender, peripherals)).ok();
spawner.spawn(update_task(stack, display, receiver)).ok();
任务间通信

Channel(多个值)

use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Channel};

static CHANNEL: StaticCell<Channel<NoopRawMutex, Data, 3>> = StaticCell::new();
// sender.send(data).await / receiver.receive().await

Signal(单次通知)

use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal};

static SIGNAL: Signal<CriticalSectionRawMutex, ()> = Signal::new();
// SIGNAL.signal(()) / SIGNAL.wait().await
重连循环模式
'reconnect: loop {
    let mut client = initialize_client().await?;
    loop {
        match client.process().await {
            Ok(_) => { /* handle messages */ }
            Err(e) => {
                println!("Error: {:?}", e);
                continue 'reconnect;  // Reconnect on error
            }
        }
    }
}

电源管理

深度睡眠配置
use esp_hal::rtc_cntl::{Rtc, sleep::{RtcSleepConfig, TimerWakeupSource, RtcioWakeupSource, WakeupLevel}};

pub fn enter_deep(wakeup_pin: &mut dyn RtcPin, rtc_cntl: LPWR, duration: Duration) -> ! {
    // GPIO wake source
    let wakeup_pins: &mut [(&mut dyn RtcPin, WakeupLevel)] = &mut [(wakeup_pin, WakeupLevel::Low)];
    let ext0 = RtcioWakeupSource::new(wakeup_pins);

    // Timer wake source
    let timer = TimerWakeupSource::new(duration.into());

    let mut rtc = Rtc::new(rtc_cntl);
    let mut config = RtcSleepConfig::deep();
    config.set_rtc_fastmem_pd_en(false);  // Keep RTC fast memory powered

    rtc.sleep(&config, &[&ext0, &timer]);
    unreachable!();
}
RTC 快速内存持久化
use esp_hal::ram;

#[ram(unstable(rtc_fast))]
pub static BOOT_COUNT: RtcCell<u32> = RtcCell::new(0);

// Survives deep sleep - read/write with .get()/.set()
let count = BOOT_COUNT.get();
BOOT_COUNT.set(count + 1);
电源优化
  • 仅在读取期间切换传感器电源引脚
  • 在显示屏上使用省电模式
  • 睡眠前正常断开 WiFi
  • 尽量缩短唤醒时长

WiFi 网络

连接设置
// esp_radio::init() is GONE — wifi::new() is called directly (as of esp-radio 0.18+)
use esp_radio::wifi::{self, ClientConfig, ModeConfig, WifiController};

let (mut controller, interfaces) =
    esp_radio::wifi::new(peripherals.WIFI, Default::default())
        .expect("Failed to initialize Wi-Fi controller");

let client_config = ModeConfig::Client(
    ClientConfig::default()
        .with_ssid(env!("WIFI_SSID").try_into().unwrap())
        .with_password(env!("WIFI_PSK").try_into().unwrap()),
);

controller.set_config(&client_config)?;
controller.start_async().await?;
controller.connect_async().await?;
Embassy-Net 协议栈
use embassy_net::{Config, Stack, StackResources};

let config = Config::dhcpv4(DhcpConfig::default());
let (stack, runner) = embassy_net::new(wifi_interface, config, stack_resources, seed);

// Wait for link and IP
loop {
    if stack.is_link_up() { break; }
    Timer::after(Duration::from_millis(500)).await;
}

loop {
    if let Some(config) = stack.config_v4() {
        println!("IP: {}", config.address);
        break;
    }
    Timer::after(Duration::from_millis(500)).await;
}
正常关闭 WiFi
pub static WIFI_SIGNAL: Signal<CriticalSectionRawMutex, ()> = Signal::new();

// In connection task
WIFI_SIGNAL.wait().await;
controller.stop_async().await?;

// Before deep sleep
WIFI_SIGNAL.signal(());

传感器模式

带预热的 ADC 采样
async fn sample_adc_with_warmup<PIN, ADC>(
    adc: &mut Adc<ADC, Blocking>,
    pin: &mut AdcPin<PIN, ADC>,
    warmup_ms: u64,
) -> Option<u16> {
    Timer::after(Duration::from_millis(warmup_ms)).await;
    nb::block!(adc.read_oneshot(pin)).ok()
}
电源控制的传感器读取
async fn read_sensor(adc: &mut Adc, pin: &mut AdcPin, power: &mut Output) -> Option<u16> {
    power.set_high();
    let result = sample_adc_with_warmup(adc, pin, 50).await;
    power.set_low();
    result
}
抗异常值平均
fn calculate_average<T: Copy + Ord + Into<u32>>(samples: &mut [T]) -> Option<T> {
    if samples.len() <= 2 { return None; }

    samples.sort_unstable();
    let trimmed = &samples[1..samples.len() - 1];  // Remove min/max

    let sum: u32 = trimmed.iter().map(|&x| x.into()).sum();
    (sum / trimmed.len() as u32).try_into().ok()
}

显示屏集成

ST7789 并行接口
use mipidsi::{Builder, options::ColorInversion};

let di = display_interface_parallel_gpio::Generic8BitBus::new(/*pins*/);
let mut display = Builder::new(ST7789, di)
    .display_size(320, 170)
    .invert_colors(ColorInversion::Inverted)
    .init(&mut delay)?;
省电模式
display.set_display_on(false)?;  // Enter power save
// Before deep sleep
power_pin.set_low();

错误处理

模块错误模式
#[derive(Debug)]
pub enum Error {
    Wifi(WifiError),
    Display(display::Error),
    Mqtt(MqttError),
}

impl From<WifiError> for Error {
    fn from(e: WifiError) -> Self { Self::Wifi(e) }
}
可失败的主程序模式
// Note: #[esp_rtos::main] (not use esp_rtos::main; #[main])
// Note: main now returns `-> !` (diverging)
#[esp_rtos::main]
async fn main(spawner: Spawner) -> ! {
    if let Err(error) = main_fallible(spawner).await {
        log::error!("Fatal: {:?}", error);
        esp_hal::system::software_reset();
    }
    unreachable!()
}

async fn main_fallible(spawner: Spawner) -> Result<(), Error> {
    // Application logic with ? operator
}

依赖项更新

安全更新流程
cargo outdated
cargo update -p esp-hal
cargo build --release
cargo clippy -- -D warnings
破坏性变更模式
  • GPIO API 经常变更(OutputConfig)
  • 计时器初始化方式变更
  • 功能标志重命名
  • esp-rtos 0.3.0:esp_rtos::start() 现在需要 2 个参数(计时器 + 软件中断)
  • esp-radio 0.18.0:已移除 esp_radio::init();请直接调用 esp_radio::wifi::new()
  • smoltcp:现在是直接依赖项,不再是 esp-radio 的功能
  • heap_allocator!:使用 #[esp_hal::ram(reclaimed)] 替代 #[unsafe(link_section = ".dram2_uninit")]
  • 务必查看 esp-hal 发布说明,并以 esp-generate 生成的模板为参考进行迁移
版本对齐

一起更新 Embassy crate:

cargo update -p embassy-executor -p embassy-time -p embassy-sync -p embassy-net
esp-generate(项目脚手架工具)
# Install
cargo install esp-generate --locked

# Generate ESP32-S3 project with Embassy + WiFi + alloc
esp-generate --chip esp32s3 -o unstable-hal -o embassy -o alloc -o wifi -o log my-project

# Available options: unstable-hal, alloc, wifi, ble-trouble, embassy,
#   probe-rs, defmt, log, esp-backtrace, embedded-test, wokwi, ci,
#   vscode, neovim, helix, zed

注意:wifi 选项现在要求同时启用 unstable-hal 和 alloc。
注意:embassy 选项需要 unstable-hal。

调试

串口日志
use esp_println::println;
// New: reads RUST_LOG at compile time via esp-config (.cargo/esp-config.toml)
esp_println::logger::init_logger_from_env();
// Or explicitly set a level:
// esp_println::logger::init_logger(log::LevelFilter::Info);
log::info!("Debug: value = {}", value);
println!("Debug: value = {}", value); // also works
常见运行时问题
  • WiFi 失败:检查 2.4GHz 网络和信号强度
  • MQTT 失败:确认 DNS 解析和代理凭据
  • 传感器故障:检查预热延迟和电源引脚切换
  • 显示屏无显示:确保 GPIO15 为 HIGH(启用电源)
  • 睡眠唤醒失败:确认 RTC 快速内存配置
软件重置
use esp_hal::system::software_reset;
software_reset();  // Clean restart on unrecoverable error

评论

支持 Markdown