🦀 Rust + ESP32-S3 入门第一课:Hello World
作者 :CXi
硬件 :ESP32-S3R8N8 嘉立创开发板
语言 :Rust(esp-hal 硬件抽象层)
难度 :⭐ 入门级
适合人群:有 Rust 基础,想入门嵌入式开发的同学
📖 目录
1. 项目简介
本教程将带你用 Rust 语言在 ESP32-S3 开发板上跑通第一个程序:
每隔 500ms,通过 RTT 调试输出打印一行计数
你好,第 0 次
你好,第 1 次
你好,第 2 次
...
这个项目只有 38 行代码,但能帮你理解嵌入式 Rust 的核心概念:
| 知识点 | 你在代码中会看到 |
|---|---|
#![no_main] / #![no_std] |
嵌入式程序的"标准开头" |
esp_hal::init() |
初始化芯片硬件 |
loop {} |
嵌入式程序的无限主循环 |
rprintln!() |
通过 RTT 输出调试日志 |
Instant::now() |
时间测量与延时 |
2. 硬件介绍
2.1 ESP32-S3R8N8 嘉立创开发板

芯片参数:
| 参数 | 值 |
|---|---|
| 芯片 | ESP32-S3 |
| CPU | Xtensa LX7 双核,240MHz |
| Flash | 8MB(N8) |
| PSRAM | 8MB(R8) |
| WiFi | 802.11 b/g/n |
| 蓝牙 | BLE 5.0 |
2.2 你需要准备什么
- ✅ ESP32-S3R8N8 嘉立创开发板 × 1
- ✅ Type-C USB 数据线 × 1
- ✅ 电脑(Windows / macOS / Linux)
💡 本教程不涉及任何外设接线,一根 USB 线就够了。
3. 开发环境搭建
3.1 安装 Rust
bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
3.2 安装 ESP32 工具链
bash
# espup:ESP32 Rust 工具链管理器
cargo install espup
# 安装 Xtensa 工具链(支持 ESP32-S3 的 Rust 编译器)
espup install
# 烧录工具
cargo install espflash --locked
cargo install probe-rs-tools --locked
3.3 配置环境变量
bash
# Linux / macOS:每次打开终端都要执行
source $HOME/export-esp.sh
# Windows(PowerShell)
# espup install 完成后会提示环境变量路径,按提示操作即可
💡 嫌麻烦?把
source $HOME/export-esp.sh加到~/.bashrc或~/.zshrc里,自动生效。
3.4 克隆项目
bash
git clone https://github.com/cx693/Rust_ESP32_Dome.git
cd Rust_ESP32_Dome/examples/hello
4. 项目结构
Rust_ESP32_Dome/
├── README.md ← 项目说明
├── examples/
│ ├── hello/ ← ⭐ 本教程项目
│ │ ├── .cargo/
│ │ │ └── config.toml ← 构建配置(目标芯片、烧录器)
│ │ ├── src/
│ │ │ ├── bin/
│ │ │ │ └── main.rs ← ⭐ 主程序代码
│ │ │ └── lib.rs ← 库入口(本项目未使用)
│ │ ├── build.rs ← 构建脚本
│ │ ├── Cargo.toml ← 依赖声明
│ │ └── rust-toolchain.toml ← 指定 esp 工具链
│ ├── LED/ ← LED 闪烁
│ ├── 按键LED/ ← 按键控制 LED
│ └── ... ← 更多示例
└── dome/ ← 进阶项目(SPI 显示屏等)
关键配置文件
.cargo/config.toml --- 告诉 Cargo 编译到哪个芯片:
toml
[build]
target = "xtensa-esp32s3-none-elf" # ESP32-S3 的编译目标
[target.xtensa-esp32s3-none-elf]
runner = "probe-rs run --chip=esp32s3" # cargo run 时自动用 probe-rs 烧录
rust-toolchain.toml --- 指定使用 esp 工具链:
toml
[toolchain]
channel = "esp" # espup 安装的专用工具链
Cargo.toml --- 依赖声明:
toml
[dependencies]
esp-hal = { version = "~1.1.0", features = ["esp32s3"] } # 硬件抽象层
esp-bootloader-esp-idf = "0.5.0" # IDF 引导加载程序
rtt-target = "0.6" # RTT 日志输出
panic-rtt-target = "0.2" # panic 时输出错误
5. 代码逐行详解
5.1 嵌入式必需声明
rust
#![no_main]
#![no_std]
| 声明 | 作用 | 为什么需要 |
|---|---|---|
#![no_main] |
禁用标准 main 入口 |
嵌入式没有操作系统,用 #[main] 宏替代 |
#![no_std] |
禁用标准库 std |
std 依赖 OS(文件/网络/线程),嵌入式只能用 core |
📌 每个嵌入式 Rust 程序都必须有这两行,直接背下来就行。
5.2 导入依赖
rust
use esp_bootloader_esp_idf;
use esp_hal::{main, time::Instant};
use rtt_target::{rprintln, rtt_init_print};
use panic_rtt_target as _;
| 模块 | 用途 |
|---|---|
esp_bootloader_esp_idf |
ESP-IDF 引导加载程序(芯片启动用) |
esp_hal::main |
程序入口宏 |
esp_hal::time::Instant |
时间测量工具(类似 std::time::Instant) |
rtt_target::rprintln |
RTT 打印宏(类似 println!) |
rtt_target::rtt_init_print |
初始化 RTT 打印功能 |
panic_rtt_target |
panic 时通过 RTT 输出错误信息 |
📌
use panic_rtt_target as _的_表示"我不直接用这个模块的名字,但需要它生效"------这是 Rust 中引入"副作用模块"的惯用写法。
5.3 应用描述符
rust
esp_bootloader_esp_idf::esp_app_desc!();
生成 ESP-IDF 引导加载程序需要的元数据(版本、大小等)。必须有,否则烧录会失败。
5.4 程序入口
rust
#[main]
fn main() -> ! {
rtt_init_print!();
| 语法 | 含义 |
|---|---|
#[main] |
esp-hal 的入口宏,替代标准 fn main() |
-> ! |
"永不返回"类型 --- 嵌入式程序永远运行,不会退出 |
rtt_init_print!() |
初始化 RTT,之后才能用 rprintln! |
5.5 初始化硬件
rust
let _peripherals = esp_hal::init(esp_hal::Config::default());
芯片上电
│
▼
esp_hal::init()
│
├── 配置时钟系统
├── 配置电源管理
├── 初始化 GPIO 控制器
└── 返回 peripherals 对象(包含所有外设控制权)
_peripherals 的下划线前缀 _ 表示"暂时不用这个变量",但 init() 必须调用,否则硬件没初始化。
5.6 主循环
rust
let mut count: u32 = 0;
loop {
rprintln!("你好,第 {} 次", count);
count += 1;
// 忙等待延时 500ms
let start = Instant::now();
while start.elapsed().as_millis() < 500 {}
}
逐行解析:
| 行 | 代码 | 作用 |
|---|---|---|
| 1 | let mut count: u32 = 0 |
定义可变计数器 |
| 2 | loop {} |
无限循环,嵌入式程序必须有 |
| 3 | rprintln!(...) |
通过 RTT 输出文字到调试器 |
| 4 | count += 1 |
计数器自增 |
| 5 | Instant::now() |
记录当前时间戳 |
| 6 | while ... < 500 {} |
忙等待 500 毫秒 |
延时原理图:
时间轴 ─────────────────────────────────→
start = Instant::now()
│
├── 0ms start.elapsed() = 0ms → 循环继续
├── 100ms start.elapsed() = 100ms → 循环继续
├── 200ms start.elapsed() = 200ms → 循环继续
├── ...
└── 500ms start.elapsed() = 500ms → 条件不满足,跳出循环
│
▼
打印下一行文字
⚠️ 忙等待会一直占用 CPU,实际项目中推荐用定时器中断。但作为入门示例,它最容易理解。
6. 编译与烧录
6.1 编译
bash
cd Rust_ESP32_Dome/examples/hello
cargo build --release
⚠️ 嵌入式项目建议用
--release,debug 模式的二进制可能太大。
6.2 烧录
bash
cargo run --release
输出示例:
Compiling hello v0.1.0
Finished `release` profile [optimized] target(s) in 5.23s
Running `probe-rs run --chip=esp32s3 ...`
Erasing ✔ 100% [####################] 12.00 KiB @ 12.00 KiB/s
Programming ✔ 100% [####################] 12.00 KiB @ 12.00 KiB/s
INFO - ESP32-S3 启动成功!开始计数...
INFO - 你好,第 0 次
INFO - 你好,第 1 次
INFO - 你好,第 2 次
6.3 进入下载模式(如果烧录失败)
-
按住 BOOT 按键不放
-
按一下 RST 按键
-
松开 BOOT 按键
-
重新执行
cargo run --release┌─────────────────────────────────────┐ │ ESP32-S3 嘉立创开发板 │ │ │ │ [BOOT] 按键 [RST] 按键 │ │ GPIO0 EN │ │ │ │ 按住 BOOT → 按 RST → 松 BOOT │ │ = 进入下载模式 │ └─────────────────────────────────────┘
7. 常见问题
Q1:编译报错 can't find crate for core
原因:没装 ESP32 工具链
bash
espup install
source $HOME/export-esp.sh
Q2:烧录失败 Failed to connect
原因:开发板没进入下载模式
解决:按住 BOOT → 按一下 RST → 松开 BOOT → 重新烧录
Q3:看不到 RTT 日志
原因 :RTT 输出需要 probe-rs 支持,确认用的是 cargo run(不是 espflash)
排查步骤:
bash
# 确认 probe-rs 已安装
probe-rs --version
# 确认开发板连接
probe-rs info --chip esp32s3
Q4:编译很慢
原因 :嵌入式项目需要从源码编译 core 库(build-std = ["alloc", "core"]),首次编译需要 2-5 分钟
解决:耐心等待,后续编译有缓存会快很多(增量编译通常 < 10 秒)
Q5:Windows 上找不到串口
解决:安装 CP2102 或 CH340 驱动(嘉立创开发板一般用 CH340)
8. 扩展练习
| 练习 | 说明 | 难度 |
|---|---|---|
| 修改延时 | 把 500ms 改成 100ms / 1000ms,观察输出频率变化 | ⭐ |
| LED 闪烁 | 结合 GPIO48,每次打印时翻转 LED | ⭐⭐ |
| 按键计数 | 按一下 BOOT 键,count 才 +1 | ⭐⭐ |
| 温度打印 | 读取芯片内部温度传感器,打印温度值 | ⭐⭐⭐ |
| WiFi 联网 | 连接 WiFi,打印 IP 地址 | ⭐⭐⭐ |
📝 完整源码
rust
// ESP32-S3 Hello World ------ 每隔 500ms 打印一次计数
// 嵌入式程序没有操作系统,不用标准库和默认 main
#![no_main]
#![no_std]
use esp_bootloader_esp_idf;
use esp_hal::{main, time::Instant};
use rtt_target::{rprintln, rtt_init_print};
use panic_rtt_target as _;
// ESP-IDF 引导加载程序要求的应用描述,必须有
esp_bootloader_esp_idf::esp_app_desc!();
// #[main] 标记入口,`-> !` 表示永不返回(嵌入式程序永远在跑)
#[main]
fn main() -> ! {
rtt_init_print!(); // 初始化 RTT,之后才能用 rprintln!
// 初始化硬件,peripherals 包含所有外设控制权
// 下划线前缀 _ 表示"暂时不用这个变量"
let _peripherals = esp_hal::init(esp_hal::Config::default());
let mut count: u32 = 0;
// 嵌入式程序必须有无限循环
loop {
rprintln!("你好,第 {} 次", count);
count += 1;
// 忙等待延时 500ms(简单但占 CPU,实际项目建议用定时器)
let start = Instant::now();
while start.elapsed().as_millis() < 500 {}
}
}
参考资料
| 资源 | 链接 |
|---|---|
| 本项目 GitHub | https://github.com/cx693/Rust_ESP32_Dome |
| Rust on ESP 官方教程 | https://esp-rs.github.io/book/ |
| esp-hal 仓库 | https://github.com/esp-rs/esp-hal |
| ESP32-S3 数据手册 | https://www.espressif.com/sites/default/files/documentation/esp32-s3_datasheet_cn.pdf |
| esp-hal 官方示例 | https://github.com/esp-rs/esp-hal/tree/main/examples |
| probe-rs 调试工具 | https://probe.rs/ |
🎉 恭喜你跑通了第一个 Rust + ESP32 程序!
如果觉得有帮助,欢迎 👍 点赞 / ⭐ 收藏 / 💬 评论