ESP32中的Rust开发

乐鑫是少有的官方支持Rust的芯片厂商,提供了esp-idf-halesp-hal两种开发方式,

esp-idf-hal是基于esp-idf的c语言sdk做的rust封装,很多Wifi 蓝牙功能很好适配兼容,由于esp-idf 提供了newlib环境,可以在上面构建Rust标准库,所以使用esp-idf-hal的开发环境可以使用到std的方法,这样开发效率会大大提升。

esp-hal是esp32裸机的硬件抽象层(no-std)。

更多的可以参考 Rust on ESP

注意,下面的需要读者有使用过esp32C语言开发的经验才能更方便的理解

ESP32Rust环境配置

esp32分为xtensa架构和riscv架构 esp32官方提供了espup用来安装和维护esp32系列芯片所需工具链。

两行命令即可完成安装

cargo install espup
espup install

生成项目

使用esp-generate可以生成用于esp32的Rust项目

cargo install esp-generate
esp-generate --chip=esp32c6 your-project

构建和运行项目直接使用cargo命令即可

cargo build
cargo run

可以使用模板生成项目

cargo generate esp-rs/esp-idf-template cargo

开发

esp32的开发直接参考esp-idf-hal 和esp-hal的储存库示例即可,网上也有很多相关资料,在这里暂时掠过。 值得注意的是esp-hal环境下开发可以使用embassy框架,且蓝牙功能可以使用embassy新提供的trouble

混合Rust与C

很多时候大家会在“Rust or C”中纠结,这里给出一个选择:“or”,Rust和C混合编写统一在一个项目中编译,但又不完全像上面STM32需要先编译后嵌入再编译c一样,ESP提供了一些混合编程的编译脚本、工程模板在Readme中可以看到

Rust作为component ESP-IDF 方法

使用Cmake构建的方法可以参考这里

cargo generate --vcs none --git https://github.com/esp-rs/esp-idf-template cmake --name test

然后选择需要的工具链(RISC-V的才可以用nightly)

这将创建一个ESP-IDF项目,并使用Rust作为component,然后使用Cmake构建。

test/
|-- CMakeLists.txt
|-- main/
|   |-- CMakeLists.txt
|   |-- main.c
|-- sdkconfig
|-- components/
|   |-- rust-test/
|       |-- CMakeLists.txt
|       |-- placeholder.c
|       |-- build.rs
|       |-- Cargo.toml
|       |-- rust-toolchain.toml
|       |-- src/
|           |-- lib.rs

围绕Rust的代码编写和构建主要在components下的rust-test中, 模板会在lib.rs生成一个示例函数

#![allow(unused)]
fn main() {
#[no_mangle]
extern "C" fn rust_main() -> i32 {
    // It is necessary to call this function once. Otherwise some patches to the runtime
    // implemented by esp-idf-sys might not link properly. See https://github.com/esp-rs/esp-idf-template/issues/71
    esp_idf_svc::sys::link_patches();

    // Bind the log crate to the ESP Logging facilities
    esp_idf_svc::log::EspLogger::initialize_default();

    log::info!("Hello, world!");

    42
}
}

并在c中调用

extern int rust_main(void);

void app_main(void) {
    printf("Hello world from C!\n");

    int result = rust_main();

    printf("Rust returned code: %d\n", result);
}

构建和运行完全使用esp-idf csdk的方法 会编译component中的rust工程并和c工程链接

idf.py set-target [esp32|esp32s2|esp32s3|esp32c2|esp32c3|esp32c6|esp32h2]
idf.py build
idf.py -p /dev/ttyUSB0 flash
idf.py -p /dev/ttyUSB0 monitor

这样可以实时调整Rust代码的实现。同样保持了esp-idf 原来的使用方法比如components中添加其他组件库,比如把cam或者把arduino作为components添加到工程中。

利用platformio构建

很多人会使用platformio(后简称pio)去构建esp32的工程,这里也介绍一下 官方示例中给出了新建基于esp-idf构建的pio工程模板

cargo install cargo-pio #安装 pio

#cargo pio new <your-project-name> --platform espressif32 --frameworks espidf [--board <your-board-name>]

cargo pio new pio_espidf_demo --platform espressif32 --frameworks espidf --board lilygo-t-display-s3 -- --edition 2021    

板子的名字无所谓可以后续在platformio.ini中修改,adafruit_feather_esp32s3等其他板子也可以运行

在这里我们用到的是Lilygo的T-display-s3 ,大家可以用自己手边任意的esp32

而后使用

pio run

如果在vscode中可以直接图形化的选择构建和运行

也可以创建基于arduino的工程

cargo pio new pio_arduino_demo --platform espressif32 --frameworks arduino --board lilygo-t-display-s3 -- --edition 2021 

工程结构如下

├──.cargo
│  └──config.toml
├──src
│  ├──dummy.c
│  ├──lib.rs
│  └──main.cpp
├──.gitignore
├──Cargo.toml
├──platformio.cargo.py
├──platformio.git.py
├──platformio.ini
└──platformio.patch.py

提供了简单示例,lib.rs中:

#![allow(unused)]
fn main() {
// Remove if STD is supported for your platform and you plan to use it
#![no_std]

// Remove if STD is supported for your platform and you plan to use it
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

//
// Entry points
//

#[no_mangle]
extern "C" fn arduino_setup() {
}

#[no_mangle]
extern "C" fn arduino_loop() {
}

}

main.cpp中:

#include <Arduino.h>

extern "C" void arduino_setup();
extern "C" void arduino_loop();

void setup() {
    arduino_setup();
}

void loop() {
    arduino_loop();
}

默认使用no-std,由于esp32支持使用std中的Vec等库,前面提到了esp32中可以使用std环境,这里也以此为例稍作修改使用std环境 首先在lib.rs中将no-std相关依赖注释

#![allow(unused)]
fn main() {
// Remove if STD is supported for your platform and you plan to use it
// #![no_std]

// // Remove if STD is supported for your platform and you plan to use it
// #[panic_handler]
// fn panic(_info: &core::panic::PanicInfo) -> ! {
//     loop {}
// }
}

当前esp32s3需要使用espup构建我们在根目录添加一个rust-toolchain.toml文件

[toolchain]
channel = "esp"

这里然后任意添加一个打印

#![allow(unused)]
fn main() {
#[no_mangle]
extern "C" fn arduino_loop() {
    use std::string::String;
    //打印一个字符串
    let s = String::from("Hello, Rust!");
    println!("{}", s);
    
}
}

构建编译烧录然后报错

之前不是说支持std吗为什么会报错呢。 其实这里还需要再.cargo/config.toml中添加std支持,原先是

[unstable]
build-std = ["core", "panic_abort"]
build-std-features = ["panic_immediate_abort"]

修改build-std添加std支持

build-std = ["std","core", "panic_abort"]

命令行编译运行或者在vscode中图形化选择构建和运行

可以看到构建时会一并构建rust

得到串口输出

这种混合编程尤其适合在不重构现有工程的情况下添加Rust安全支持。

比如可以在官方储存库 中提供的demo上修改

仿造官方给出的pio配置在platformio.ini中添加

build_flags = 
    -DLV_LVGL_H_INCLUDE_SIMPLE
    -DARDUINO_USB_CDC_ON_BOOT=1
    ; -UARDUINO_USB_CDC_ON_BOOT
    -DDISABLE_ALL_LIBRARY_WARNINGS
    -DARDUINO_USB_MODE=1
    ; Choose different options according to different driver chips
    ; Early use of CST328
    -DTOUCH_MODULES_CST_MUTUAL
    ; Use CST816 by default
    ; -DTOUCH_MODULES_CST_SELF  

将示例factory文件夹下的几个文件复制到当前工程

src目录下结构为

├──dummy.c        
├──factory.ino    
├──factory.ino.cpp
├──factory_gui.cpp
├──factory_gui.h  
├──font_Alibaba.c 
├──lib.rs
├──lilygo1_gif.c  
├──lilygo2_gif.c  
├──pin_config.h   
└──zones.h    

我们添加几个测试函数

#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn rust_add_test(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn rust_multiply(a: i32, b: i32) -> i32 {
    a * b
}

#[no_mangle]
pub extern "C" fn rust_fibonacci(n: i32) -> i32 {
    if n <= 0 {
        return 0;
    } else if n == 1 {
        return 1;
    }
    let mut a = 0;
    let mut b = 1;
    let mut temp;
    for _ in 2..=n {
        temp = a + b;
        a = b;
        b = temp;
    }
    b
}
#[no_mangle]
pub extern "C" fn rust_find_max(arr: *const i32, len: usize) -> i32 {
    if arr.is_null() || len == 0 {
        return 0;
    }
    let slice = unsafe { std::slice::from_raw_parts(arr, len) };
    match slice.iter().max() {
        Some(&max) => max,
        None => 0,
    }
}
#[no_mangle]
pub extern "C" fn rust_string_length(s: *const u8) -> usize {
    if s.is_null() {
        return 0;
    }
    
    let mut len = 0;
    unsafe {
        while *s.add(len) != 0 {
            len += 1;
        }
    }
    len
}
}

在factory.ino中添加测试

extern "C" {
  int rust_add_test(int a, int b);
  int rust_multiply(int a, int b);
  int rust_fibonacci(int n);
  int rust_find_max(const int* arr, size_t len);
  size_t rust_string_length(const char* s);
}

void run_rust_tests(void)
{
    // 清除之前的UI元素
    lv_obj_clean(lv_scr_act());
    // 创建标题
    lv_obj_t *title_label = lv_label_create(lv_scr_act());
    lv_obj_align(title_label, LV_ALIGN_TOP_MID, 0, 10);
    lv_obj_set_style_text_font(title_label, &lv_font_montserrat_14, 0);
    lv_label_set_text(title_label, "Rust Function Tests");
    // 准备测试结果
    String result = "";
    // 测试加法函数
    int add_result = rust_add_test(10, 25);
    result += "Add: 10 + 25 = ";
    result += String(add_result);
    result += "\n";
    // 测试乘法函数
    int mul_result = rust_multiply(12, 5);
    result += "Multiply: 12 * 5 = ";
    result += String(mul_result);
    result += "\n";
    // 测试斐波那契函数
    int fib_result = rust_fibonacci(10);
    result += "Fibonacci(10) = ";
    result += String(fib_result);
    result += "\n";
    // 测试查找最大值函数
    int arr[] = {3, 7, 1, 9, 4, 6};
    int max_result = rust_find_max(arr, 6);
    result += "Array Max: ";
    result += String(max_result);
    result += "\n";
    // 测试字符串长度函数
    const char* test_str = "Hello, Rust!";
    size_t len_result = rust_string_length(test_str);
    result += "String Length: ";
    result += String(len_result);
    result += "\n";
    // 创建结果显示区域
    lv_obj_t *results_label = lv_label_create(lv_scr_act());
    lv_obj_align(results_label, LV_ALIGN_CENTER, 0, 0);
    lv_obj_set_width(results_label, LV_PCT(90));
    lv_obj_set_style_text_font(results_label, &lv_font_montserrat_14, 0);
    lv_label_set_long_mode(results_label, LV_LABEL_LONG_SCROLL);
    lv_label_set_text(results_label, result.c_str());
    // 输出到串口
    Serial.println("=== Rust Test Results ===");
    Serial.println(result);
    // 创建返回按钮
    lv_obj_t *back_btn = lv_btn_create(lv_scr_act());
    lv_obj_align(back_btn, LV_ALIGN_BOTTOM_MID, 0, -10);
    lv_obj_set_width(back_btn, 120);
    lv_obj_set_height(back_btn, 40);
    lv_obj_t *back_label = lv_label_create(back_btn);
    lv_label_set_text(back_label, "Back");
    lv_obj_center(back_label);
    lv_obj_add_event_cb(back_btn, [](lv_event_t *e) {
        lv_obj_clean(lv_scr_act());
        lv_obj_t *log_label = lv_label_create(lv_scr_act());
        lv_obj_align(log_label, LV_ALIGN_TOP_LEFT, 0, 0);
        lv_obj_set_width(log_label, LV_PCT(100));
        lv_label_set_long_mode(log_label, LV_LABEL_LONG_SCROLL);
        lv_label_set_recolor(log_label, true);
        lv_label_set_text(log_label, "Scan WiFi");
        wifi_test();
    }, LV_EVENT_CLICKED, NULL);
    // 发送测试结果消息
    lv_msg_send(MSG_RUST_TEST_RESULT, result.c_str());
}

void rust_tests(void)
{
    // 清除当前屏幕
    lv_obj_clean(lv_scr_act());
    // 显示加载指示器
    lv_obj_t *spinner = lv_spinner_create(lv_scr_act(), 1000, 60);
    lv_obj_set_size(spinner, 100, 100);
    lv_obj_center(spinner);
    lv_obj_t *load_label = lv_label_create(lv_scr_act());
    lv_label_set_text(load_label, "Running Rust Tests...");
    lv_obj_align(load_label, LV_ALIGN_BOTTOM_MID, 0, -40);
    // 短暂延迟以显示加载效果
    LV_DELAY(1000);
    // 删除加载指示器
    lv_obj_del(spinner);
    lv_obj_del(load_label);
    // 运行详细的Rust测试
    run_rust_tests();
}

可以看到开发板屏幕将显示测试的结果

至此对于ESP32的Rust和C混合编程基础就完成了,可以开始进行自己的项目了。

也可以使用类似stm32中的方法找到target中的.a文件复制到其他项目中使用

复制到其他项目目录中,并在build_flags中添加依赖

利用这个方法可以在比如T-Display-S3-Pro的Cellphone项目中添加RustTest按钮

把.a文件放在根目录,在platformio.ini中添加

    -L"${PROJECT_DIR}"  
    -lpio_arduino_demo

然后类似添加按钮和函数即可得到

点击按钮可以看到运行示例

和串口输出

至此整个指南便结束 下面开启你的嵌入式Rust之旅吧!