Hotdry.

Article

ESP32-S3 双核裸金属 Rust 与 ESP-IDF 并行运行:内存布局与跨核通信工程参数

详解 ESP32-S3 双核上裸金属 Rust 与 ESP-IDF 的协同运行方案,提供内存预留、硬件启动序列、原子通信与构建配置的完整工程参数。

2026-04-26systems

在 ESP32-S3 开发中,Wi-Fi、BLE 和外设驱动通常依赖 ESP-IDF(基于 FreeRTOS),而业务逻辑和计算密集型任务更适合用 Rust 编写。ESP32-S3 的双核架构提供了一个优雅的分离方案:让 Core 0 运行 ESP-IDF 负责系统任务,Core 1 运行裸金属 Rust 代码处理实时计算,两者通过共享内存进行数据交换。本文给出这一方案的完整工程参数,覆盖内存布局、硬件启动序列、跨核通信机制和构建配置。

双核架构与内存规划

ESP32-S3 搭载两颗 Xtensa LX7 处理器核心,编号为 Core 0(PRO_CPU)和 Core 1(APP_CPU)。默认情况下,ESP-IDF 的 FreeRTOS 在两个核心上调度任务,但通过 CONFIG_FREERTOS_UNICORE=y 可以让 FreeRTOS 只运行在 Core 0,Core 1 处于空闲状态可供裸金属代码使用。

双核共享同一片 SRAM 空间,但需要为 Core 1 的裸金属代码预留专用内存区域,防止 ESP-IDF 的堆分配器使用这部分 RAM。ESP-IDF 提供 SOC_RESERVE_MEMORY_REGION 宏来声明保留区域:

#include "heap_memory_layout.h"
// 预留 128KB 从 0x3FCC9710 到 0x3FCE9710
SOC_RESERVE_MEMORY_REGION(0x3FCC9710, 0x3FCE9710, rust_app);

这段 128KB 的内存用于 Core 1 的栈空间和全局变量存储。栈在 Xtensa 架构上向低地址生长,因此栈顶(初始栈指针)取高地址 0x3FCE9710。在链接脚本中定义这一符号供汇编 trampoline 使用:

_rust_stack_top = 0x3FCE9710;

如果采用运行时加载方案(后续详述),Rust 二进制文件存放在独立的 Flash 分区,需要通过 MMU 映射到虚拟地址空间。典型的配置是将 Flash 分区映射到 0x42400000 起的位置:

项目 地址 / 大小 说明
保留 RAM 起始 0x3FCC9710 Core 1 栈与数据区
保留 RAM 结束 0x3FCE9710 128KB 总量
Rust 二进制虚拟地址 0x42400000 MMU 映射目标
Flash 分区偏移 0x200000 2MB 位置
Flash 分区大小 0x80000 512KB

Core 1 硬件启动序列

Core 1 不像 Core 0 那样自动执行 ROM 启动代码,需要 Core 0 通过硬件寄存器手动唤醒。ESP32-S3 的系统控制寄存器位于 SYSTEM_CORE_1_CONTROL_0_REG,通过位掩码操作完成时钟使能、解除 stall 和复位脉冲:

#include "soc/system_reg.h"

// 1. 设置 Core 1 的启动地址(ROM 函数)
ets_set_appcpu_boot_addr((uint32_t)app_core_trampoline);

// 2. 使能时钟门
SET_PERI_REG_MASK(SYSTEM_CORE_1_CONTROL_0_REG,
                  SYSTEM_CONTROL_CORE_1_CLKGATE_EN);

// 3. 解除 stall(Core 1 从冻结状态恢复)
CLEAR_PERI_REG_MASK(SYSTEM_CORE_1_CONTROL_0_REG,
                    SYSTEM_CONTROL_CORE_1_RUNSTALL);

// 4. 发送复位脉冲
SET_PERI_REG_MASK(SYSTEM_CORE_1_CONTROL_0_REG,
                  SYSTEM_CONTROL_CORE_1_RESETING);
CLEAR_PERI_REG_MASK(SYSTEM_CORE_1_CONTROL_0_REG,
                    SYSTEM_CONTROL_CORE_1_RESETING);

启动地址指向一个极简的汇编 trampoline,其职责是在 Core 1 真正进入 Rust 代码之前设置栈指针。Xtensa 架构使用 a1 寄存器作为栈指针:

    .section .iram1, "ax"
    .global app_core_trampoline
    .align 4
app_core_trampoline:
    movi  a1, _rust_stack_top    ; 加载栈顶地址
    call0 rust_app_core_entry    ; 跳入 Rust 入口,永不返回

汇编代码必须放在 .iram1 段,因为 Core 1 复位后可能尚未完成 Flash 缓存配置,IRAM 中的代码始终可执行。

跨核通信:原子共享内存

双核之间的数据交换无需复杂的 IPC 机制,因为它们共享同一片物理内存。关键在于使用原子操作避免竞争条件。Rust 的 AtomicU32 提供 CPU 级别的原子性保证,不需要互斥锁:

use core::sync::atomic::{AtomicU32, Ordering};

#[unsafe(no_mangle)]
pub static RUST_CORE1_COUNTER: AtomicU32 = AtomicU32::new(0);

#[unsafe(no_mangle)]
pub extern "C" fn rust_app_core_entry() -> ! {
    loop {
        RUST_CORE1_COUNTER.fetch_add(1, Ordering::Relaxed);
        for _ in 0..1_000_000 {
            core::hint::spin_loop();
        }
    }
}

对于静态链接方案,C 代码可以直接通过符号名访问该变量:

extern volatile uint32_t RUST_CORE1_COUNTER;

void app_main(void) {
    while (1) {
        ESP_LOGI("Core 0", "Counter: %lu", (unsigned long)RUST_CORE1_COUNTER);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

对于运行时加载方案(二进制独立编译),链接阶段不再有共享符号,此时需要双方约定固定内存地址。C 端通过指针直接读取:

#define RUST_COUNTER_ADDR 0x3FCC9710
volatile uint32_t *counter = (volatile uint32_t *)RUST_COUNTER_ADDR;

Ordering::Relaxed 适用于纯计数器场景,因为不需要额外的内存顺序保证。如果需要更复杂的通信协议(如命令 / 响应语义),可考虑 Ordering::Release / Acquire 配对,确保写操作对另一核心可见。

构建配置与两种运行模式

该方案支持两种运行模式:编译时静态链接和运行时动态加载。

模式一:静态链接

Rust 代码编译为静态库(.a 文件),在 ESP-IDF 链接阶段合并进固件。Cargo.toml 配置:

[lib]
crate-type = ["staticlib"]

[profile.release]
opt-level = "s"
codegen-units = 1
lto = "fat"
overflow-checks = false

ESP-IDF 的 CMakeLists.txt 将 Rust 库链接进来:

idf_component_register(SRC "main.c" "app_core_trampoline.S")
add_prebuilt_library(rust_app "${CMAKE_CURRENT_SOURCE_DIR}/lib/libesp_rust_app.a")
target_link_libraries(${COMPONENT_LIB} INTERFACE rust_app)
target_link_options(${COMPONENT_LIB} INTERFACE "-T${CMAKE_CURRENT_SOURCE_DIR}/rust_stack.ld")

构建流程:

cargo build --release --target xtensa-esp32s3-none-elf
cp target/xtensa-esp32s3-none-elf/release/libesp_rust_app.a main/lib/
idf.py build flash monitor

模式二:运行时加载

Rust 代码编译为独立 ELF,存放于独立 Flash 分区。Core 0 在启动 Core 1 前通过 MMU 将该分区映射到虚拟地址空间:

#include "esp_partition.h"
#include "hal/mmu_hal.h"
#include "hal/cache_hal.h"

#define RUST_VADDR 0x42400000

static void load_rust_app(void) {
    const esp_partition_t *part = esp_partition_find_first(
        ESP_PARTITION_TYPE_DATA, 0x40, "rust_app");
    
    uint32_t page_size = CONFIG_MMU_PAGE_SIZE;
    uint32_t pages = (part->size + page_size - 1) / page_size;
    for (uint32_t i = 0; i < pages; i++) {
        mmu_hal_map_region(0, MMU_TARGET_FLASH0,
                           RUST_VADDR + (i * page_size),
                           part->address + (i * page_size),
                           page_size, &(uint32_t){0});
    }
    cache_hal_invalidate_addr(RUST_VADDR, part->size);
}

Flash 分区表配置(partitions.csv):

nvs,      data, nvs,     0x9000,  0x6000
phy_init, data, phy,     0xf000,  0x1000
factory,  app,  factory, 0x10000, 0x1F0000
rust_app, data, 0x40,    0x200000, 0x80000

独立构建和烧录:

# Rust 侧
cargo build --release --target xtensa-esp32s3-none-elf
xtensa-esp32s3-elf-objcopy -O binary target/xtensa-esp32s3-none-elf/release/esp_rust_app rust_app.bin

# 烧录 Rust 二进制
esptool.py --port /dev/ttyACM0 write_flash 0x200000 rust_app.bin

工程实践要点

内存边界检查:使用 SOC_RESERVE_MEMORY_REGION 后,ESP-IDF 的 heap 初始化日志会显示保留区域不再可用。启动后应验证 0x3FCC9710 未出现在堆可用列表中。

调试与看门狗:裸金属 Core 1 没有任务调度器,若进入死循环或硬件异常,FreeRTOS 无法接管。需在 Core 0 侧定期检查 Core 1 的心跳(如计数器超时),并实现复位逻辑。ESP32-S3 的任务看门狗(Task Watchdog)默认仅监控 FreeRTOS 任务,裸金属代码需自行处理异常检测。

中断处理:裸金属 Core 1 可以独立配置中断,但需注意外设中断默认路由到 Core 0。需要在 start_rust_on_app_core 之前通过 esp_intr_alloc 将特定中断 affinity 设置为 Core 1。

二进制热更新:运行时加载模式的核心优势在于可以单独更新 Rust 程序而不影响 ESP-IDF 固件。实现上只需将新二进制写入 Flash 分区地址 0x200000,然后重新执行 Core 1 启动序列即可。

小结

ESP32-S3 的双核架构为混合编程范式提供了天然边界。Core 0 承载成熟的 ESP-IDF 无线栈和 FreeRTOS 调度器,Core 1 运行裸金属 Rust 实现零调度干扰的实时处理。通过 SOC_RESERVE_MEMORY_REGION 预留 128KB 专用 RAM、使用硬件寄存器序列启动 Core 1、借助 AtomicU32 在固定地址进行无锁通信,可构建一套可靠的双核协同系统。静态链接适合固件一次性交付场景,运行时加载则为 OTA 更新和用户可编程逻辑提供了灵活性。

资料来源:Tingou Wu, Running Bare-Metal Rust Alongside ESP-IDF on the ESP32-S3's Second Core, https://tingouw.com/blog/embedded/esp32/run_rust_on_app_core

systems