第 2 章 开发环境搭建
【学习目标】
安装 Rust 工具链(
rustup)与 ESP 专用工具链(espup),理解稳定版/夜间版的取舍;安装
cargo-generate、espflash、probe-rs等辅助工具;用官方模板生成第一个
no_std工程,完成编译、烧录与串口监视;能独立排查烧录失败、链接错误、环境变量等常见问题。
2.1 安装 Rust 与工具链
2.1.1 安装 rustup
rustup 是 Rust 官方的工具链管理器。
Linux / macOS(打开终端):
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
安装过程中一路选默认(1) Proceed with installation)即可。完成后重新打开终端,或手动加载环境:
source "$HOME/.cargo/env"
Windows:从 https://rustup.rs 下载 rustup-init.exe 运行。建议同时安装 Visual Studio 的 "C++ 桌面开发" 组件(提供链接器),或按提示使用 MSVC 工具。
验证安装:
rustc --version # rustc 1.8x.x (...)
cargo --version
稳定版还是夜间版? 本书全部使用 stable。Rust 每 6 周发布一个稳定版,esp-hal 官方也以稳定版为支持目标。只有个别高级场景(某些 #![feature(...)] 实验特性)才需要 nightly——遇到时书中会明确说明,并给出 rust-toolchain.toml 的写法。
2.1.2 用 espup 安装 ESP 工具链
espup 是乐鑫官方的"一键全家桶"安装器:它会装好交叉编译目标、ESP 版调试工具(OpenOCD fork)、并配置好环境变量。
# 1) 安装 espup 本身(它是一个 Rust 写的工具)
cargo install espup
# 2) 安装 ESP 全家桶(RISC-V/Xtensa 目标、OpenOCD 等)
espup install
# 3) 导出环境变量(每个新终端都要执行一次,或写进 shell 配置)
# Linux / macOS:
. $HOME/export-esp.sh
# 或者把这行加进 ~/.bashrc / ~/.zshrc:
# source "$HOME/export-esp.sh"
Windows 上用 espup export --format powershell(或 cmd),把输出的命令粘贴到终端执行。
如果
espup install只关心 RISC-V 芯片(如 ESP32-C6),可以加--targets riscv跳过 Xtensa 目标,省下载时间。
编译目标三元组。裸机固件要编译到芯片的目标三元组,ESP32-C6 是:
riscv32imac-unknown-none-elf
拆开看:riscv32 = 32 位 RISC-V;i = 基础整数指令集;m = 乘除法扩展;a = 原子指令扩展;c = 压缩指令扩展;unknown = 无操作系统厂商;none = 裸机(无 std);elf = 输出 ELF 格式。这个名字精确描述了 ESP32-C6 的 HP 核。espup 已经替我们装好了它,你可以用 rustup target list --installed 确认。
2.2 安装辅助工具
# 工程模板生成器(第 2.3 节用)
cargo install cargo-generate
# 烧录 + 串口监视(独立二进制,推荐)
cargo install espflash
# 片上调试工具(第 14 章详细用)
cargo install probe-rs-tools
# ELF 分析工具(第 5 章用,依赖 LLVM 工具)
cargo install cargo-binutils
几点说明:
-
espflash也可以作为 cargo 子命令安装(cargo install cargo-espflash,然后用 cargo espflash flash)。两者功能一致,本书命令以独立二进制 espflash为准; -
probe-rs-tools提供 probe-rs命令行:它能通过 ESP32-C6 板载的 USB-Serial/JTAG 直接调试,不需要外接调试器; -
国内网络下载慢的话,可以给 cargo 换国内镜像源(配置 ~/.cargo/config.toml的 [source]段,具体镜像以最新可用为准)。
2.3 创建第一个 no_std 模板项目
2.3.1 用 esp-template 生成工程
cargo generate gh:esp-rs/esp-template
生成器会交互式地问你一串问题,按下面选择(不同模板版本问法略有差异,含义一致):
|
问题 |
选择 |
说明 |
|---|---|---|
|
Project Name |
|
工程名 |
|
Chip |
|
目标芯片 |
|
Dev board |
|
有对应选项就选开发板 |
|
|
stable 或 unstable 均可 |
选 unstable 可体验更多驱动 |
|
Enable logging via defmt? |
yes |
日志框架,第 9 章详解 |
|
Configure project for VSCode? |
按喜好 |
生成 .vscode 配置 |
几秒钟后得到 hello-c6/ 工程,结构大致如下:
hello-c6/
├── Cargo.toml # 依赖与 features(esp-hal、esp-println、esp-backtrace...)
├── build.rs # 构建脚本:把元数据(芯片、flash 大小等)传给链接脚本
├── .cargo/
│ └── config.toml # 交叉编译目标 + 烧录 runner + 链接参数
├── rust-toolchain.toml # 锁定工具链(可能有)
└── src/
└── main.rs # 最小固件
三个关键文件值得现在就看一眼。
.cargo/config.toml——告诉 cargo "默认编译到哪、编译完用什么跑":
[build]
target = "riscv32imac-unknown-none-elf" # 默认交叉编译目标
[target.riscv32imac-unknown-none-elf]
runner = "espflash flash --monitor" # cargo run 时自动烧录+监视串口
rustflags = [
"-C", "link-arg=-Tlink.x", # 使用嵌入式运行时提供的链接脚本
]
Cargo.toml——依赖区会看到熟悉的三件套(版本号以模板生成结果为准):
[dependencies]
esp-hal = { version = "...", features = ["esp32c6", "unstable"] }
esp-println = { version = "...", features = ["esp32c6", "defmt"] }
esp-backtrace = { version = "...", features = ["esp32c6", "panic-handler", "defmt"] }
src/main.rs——最小固件(下一章逐行剖析):
#![no_std]
#![no_main]
use esp_backtrace as _;
use esp_println::println;
#[esp_hal::main]
fn main() -> ! {
println!("Hello, ESP32-C6!");
loop {}
}
2.3.2 编译、烧录、监视
把开发板的 USB 口(USB-Serial/JTAG 那个) 插上电脑,然后:
cd hello-c6
cargo run
一条命令完成三件事:
-
编译到
riscv32imac-unknown-none-elf; -
espflash烧录(自动找串口;必要时按住 BOOT 再点 RESET 进下载模式); -
烧录完成后自动复位并打开串口监视。
看到类似输出即成功:
Flashing to /dev/ttyACM0 (esp32c6)
...
Finished in 1.2s
ESP-ROM:esp32c6-...
Hello, ESP32-C6!
按 Ctrl+] 退出监视。之后随时可以只监视:espflash monitor;只烧录不监视:espflash flash target/riscv32imac-unknown-none-elf/release/hello-c6。
恭喜:你已经跑通了嵌入式开发最小闭环。后面 16 章都是在这个闭环上叠加内容。
2.4 常见问题排查
先记住两个通用自救动作:换一根数据线(很多线只能充电);按住 BOOT + 短按 RESET 手动进下载模式。
|
|
|
|
|---|---|---|
No serial ports found |
没进下载模式;只有充电线;驱动缺失 |
|
Permission denied: /dev/ttyACM0 |
当前用户无串口权限 |
sudo usermod -aG dialout $USER 后注销重登(或临时 sudo chmod 666 /dev/ttyACM0)
|
Failed to connect |
波特率太高、USB 集电器供电不足 |
espflash flash --baud 460800 ... 降低波特率;直连主板 USB 口
|
error: linking with rust-lld failed / 找不到符号
|
工具链/目标没装全; |
espup install 重装;对照模板的 .cargo/config.toml |
can't find crate for core(目标相关)
|
编译目标未安装 |
espup install(它负责装 riscv32imac-unknown-none-elf);或 rustup target add riscv32imac-unknown-none-elf |
esp32c6 feature 相关错误
|
依赖的 chip feature 不匹配 |
esp-hal/esp-println/esp-backtrace 都启用了 esp32c6 |
espup/xtensa 相关命令找不到
|
环境变量没加载 |
source $HOME/export-esp.sh(Linux/macOS)或 espup export(Windows)
|
|
|
监视的串口不对(板子有两个 USB 口) |
espflash monitor 会列出候选串口
|
cargo run 每次都提示 rebuild
|
环境变量/时间戳变化、不同 shell 配置 |
|
一个调试心法:把报错原文完整读一遍。Rust 的链接/编译错误几乎总是把缺的东西写出来了(缺哪个符号、哪个 crate、哪个 feature);对照 附录 C 的常见错误表,绝大多数环境类问题都有现成答案。
Rust 语法加油站
条件编译:#[cfg(...)] 与 cfg!
嵌入式代码经常"同一份源码、多种芯片/多种配置"。Rust 用条件编译在编译期裁剪代码:
// 属性形式:整块代码按条件保留或丢弃
#[cfg(feature = "defmt")]
use defmt::info as log_info;
#[cfg(not(feature = "defmt"))]
fn log_info(msg: &str) { /* 什么都不做 */ }
// 也可以修饰表达式、类型、参数
fn setup() {
#[cfg(target_os = "none")]
init_hardware();
}
-
条件可以是:
feature = "xxx"、target_os = "none"(裸机!)、target_arch = "riscv32"、自定义 cfg 键等; -
#[cfg]作用于其后的一项(函数、模块、语句……);#[cfg_attr(...)]还能按条件加属性。
cfg! 是它的宏形式,返回布尔值,用于运行时(其实也会被编译期优化掉):
if cfg!(feature = "defmt") {
println!("defmt 已启用");
}
区别:#[cfg] 直接删掉代码(被删的代码不参与编译,写错了也不报错);cfg! 只是条件判断,两个分支都要能编译。
嵌入式里的典型用途:同一驱动支持多颗芯片(#[cfg(feature = "esp32c6")])、有屏/无屏两种板型、测试与固件两种构建。esp-hal 自己就是靠一堆 cfg 支持整个 ESP 家族的。
特性(features)管理
feature 是 Cargo 的可选编译开关,在 Cargo.toml 定义:
[features]
default = ["wifi"] # 默认启用的集合
wifi = ["dep:esp-wifi"] # 启用后会带上可选依赖
debug-logs = [] # 纯开关,代码里用 #[cfg(feature = "debug-logs")]
使用方(比如我们的固件)这样启用/关闭:
[dependencies]
some-lib = { version = "1.0", features = ["wifi"], default-features = false }
要点与陷阱:
-
可选依赖:
optional = true的依赖配合 dep:名字写进 feature,实现"要这个功能才编译这个依赖"——esp-hal对 esp32c6的支持本质就是芯片 feature + cfg; -
feature 并集(unification):同一个 crate 被多个依赖引用时,feature 会取并集。两个依赖一个要
esp32c6、一个要 esp32s3,就会编译报错——这就是"一颗固件只能服务一颗芯片"的编译期保护; -
少即是多:只启用需要的 feature,编译更快、固件更小。第 18 章瘦身时还会回来用这招。
本章小结
-
环境 =
rustup(Rust 本体)+ espup(ESP 工具链与环境变量)+ espflash(烧录)+ probe-rs(调试)+ cargo-generate(模板)。 -
目标三元组
riscv32imac-unknown-none-elf精确描述了 C6 的裸机 RV32IMAC 核;.cargo/config.toml把它设为默认,并把 espflash flash --monitor设为 runner,于是一条 cargo run= 编译+烧录+监视。 -
遇到环境问题:先读完整报错,再查本章表格与 附录 C。
-
语法上认识了
#[cfg]/cfg!与 Cargo features——它们是理解 esp-hal多芯片支持方式的钥匙。
思考与练习
-
解释
riscv32imac-unknown-none-elf每个片段的含义;如果你的板子是 ESP32-S3(Xtensa 核),目标三元组会有什么不同? -
cargo run在普通桌面项目和在模板工程里的行为为什么不一样?(提示:.cargo/config.toml的 runner) -
#[cfg(feature = "x")]与 if cfg!(feature = "x")在"被裁掉的分支里有编译错误"时表现有何不同?动手验证一下。 -
动手:把
main.rs里打印的文字改成你的名字,重新 cargo run;再故意删掉 rustflags里的 -Tlink.x编译一次,读一读链接错误长什么样(然后改回来)。