《从零开始的ESP32-C6开发 Rust嵌入式》02

第 2 章 开发环境搭建

【学习目标】

  1. 安装 Rust 工具链(‎rustup)与 ESP 专用工具链(‎espup),理解稳定版/夜间版的取舍;

  2. 安装 ‎cargo-generate、‎espflash、‎probe-rs 等辅助工具;

  3. 用官方模板生成第一个 ‎no_std 工程,完成编译、烧录与串口监视;

  4. 能独立排查烧录失败、链接错误、环境变量等常见问题。


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

‎hello-c6

工程名

Chip

‎esp32c6

目标芯片

Dev board

‎esp32c6-devkitc-1(或 none)

有对应选项就选开发板

‎esp-hal version

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

一条命令完成三件事:

  1. ‎编译到 ‎riscv32imac-unknown-none-elf;

  2. ‎espflash烧录(自动找串口;必要时按住 BOOT 再点 RESET 进下载模式);

  3. 烧录完成后自动复位并打开串口监视。

看到类似输出即成功:

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

没进下载模式;只有充电线;驱动缺失

手动进下载模式;换数据线;Windows 下为 UART 桥安装驱动(USB-Serial/JTAG 口一般免驱)

Linux 下 ‎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 / 找不到符号

工具链/目标没装全;‎rustflags 缺 ‎-Tlink.x

‎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 多芯片支持方式的钥匙。

思考与练习

  1. 解释 ‎riscv32imac-unknown-none-elf 每个片段的含义;如果你的板子是 ESP32-S3(Xtensa 核),目标三元组会有什么不同?

  2. ‎cargo run 在普通桌面项目和在模板工程里的行为为什么不一样?(提示:‎.cargo/config.toml 的 ‎runner)

  3. ‎#[cfg(feature = "x")] 与 ‎if cfg!(feature = "x") 在"被裁掉的分支里有编译错误"时表现有何不同?动手验证一下。

  4. 动手:把 ‎main.rs 里打印的文字改成你的名字,重新 ‎cargo run;再故意删掉 ‎rustflags 里的 ‎-Tlink.x 编译一次,读一读链接错误长什么样(然后改回来)。

发表评论