【ESP32-S3 Rust 入门】ADC 模拟电压采集(带校准)
作者:CXi
日期:2025-06-16
硬件:ESP32-S3R8N8 嘉立创开发板
框架:esp-hal v1.1.0(Rust 原生 HAL)
一、项目简介
本教程将带你用 Rust 在 ESP32-S3 上读取外部模拟电压 ,使用芯片内置的 ADC(模数转换器) 将连续的电压信号转为数字值,并通过校准提高测量精度。
你将学到:
- ADC 的工作原理与关键参数(分辨率、衰减、校准)
- ESP32-S3 的 ADC 引脚分布与选型
- 如何用
esp-hal配置 ADC 并读取电压 - RTT 调试输出的使用方法
最终效果: 每 100ms 读取一次 GPIO1 的电压,终端输出类似:
ESP32-S3 ADC 校准采集示例
GPIO1: 1650 mV (1.650 V)
GPIO1: 1648 mV (1.648 V)
GPIO1: 3100 mV (3.100 V)
二、硬件介绍
2.1 嘉立创 ESP32-S3R8N8 开发板
| 参数 | 规格 |
|---|---|
| 芯片 | ESP32-S3(Xtensa LX7 双核 240MHz) |
| Flash | 8MB Quad SPI(N8) |
| PSRAM | 8MB Octal SPI(R8) |
| USB | Type-C(内置 USB-OTG / JTAG 调试) |
| GPIO | 45 个可用引脚 |
| 无线 | Wi-Fi 2.4G + BLE 5.0 |
| 工作电压 | 3.3V |
2.2 什么是 ADC?
ADC(Analog-to-Digital Converter,模数转换器) 是将连续的模拟电压信号转换为离散数字值的硬件模块。
模拟世界(连续电压) 数字世界(离散数值)
3.3V ┤ ╱╲ 4095 ┤ ╱╲
│ ╱ ╲ │ ╱ ╲
1.65 ┤──╱────╲── 2048 ┤──╱────╲──
│ ╱ ╲ │ ╱ ╲
0V ┤╱ ╲╱ 0 ┤╱ ╲╱
└──────────── 时间 └──────────── 时间
模拟信号:连续变化 ADC 输出:0~4095 的整数
2.3 ESP32-S3 ADC 引脚分布
ESP32-S3 内置 两个 12 位 SAR ADC,共 18 个通道:
| ADC | 通道 | GPIO | 备注 |
|---|---|---|---|
| ADC1 | CH0 | GPIO1 | ← 本示例使用 |
| ADC1 | CH1 | GPIO2 | |
| ADC1 | CH2 | GPIO3 | |
| ADC1 | CH3 | GPIO4 | |
| ADC1 | CH4 | GPIO5 | |
| ADC1 | CH5 | GPIO6 | |
| ADC1 | CH6 | GPIO7 | |
| ADC1 | CH7 | GPIO8 | |
| ADC2 | CH0 | GPIO9 | ⚠️ Wi-Fi 开启时不可用 |
| ADC2 | CH1 | GPIO10 | ⚠️ Wi-Fi 开启时不可用 |
| ADC2 | CH2~CH9 | GPIO11~18 | ⚠️ Wi-Fi 开启时不可用 |
⚠️ 重要限制: ADC2 与 Wi-Fi 共享硬件资源,Wi-Fi 工作时 ADC2 无法使用。如需同时用 Wi-Fi 和 ADC,只能选 ADC1 的引脚(GPIO1~GPIO8)。
2.4 衰减与量程
ADC 的输入电压不能超过参考电压。通过衰减(Attenuation) 可以扩大测量范围,代价是精度降低:
| 衰减档位 | 量程 | 精度 | 适用场景 |
|---|---|---|---|
_0dB |
0 ~ 1.1V | 最高 | 低电压精密测量 |
_2_5dB |
0 ~ 1.5V | 较高 | |
_6dB |
0 ~ 2.2V | 中等 | |
_11dB |
0 ~ 3.1V | 较低 | ← 本示例使用,范围最广 |
硬件接线: 将待测电压源(0~3.1V)接到 GPIO1 ,地线接开发板 GND。切勿超过 3.3V,否则可能烧毁芯片!
三、开发环境搭建
3.1 安装 Rust 工具链
ESP32-S3 使用 Xtensa 架构,需要 Espressif 定制的 Rust 工具链:
bash
# 1. 安装 espup(Espressif 的 Rust 工具链管理器)
cargo install espup
# 2. 安装 Xtensa 目标工具链
espup install
# 3. 使环境变量生效
source ~/.bashrc # 或 source ~/.zshrc
3.2 安装 probe-rs(烧录 & 调试工具)
bash
# 安装 probe-rs
cargo install probe-rs --features cli
# 验证安装,查看已连接的开发板
probe-rs list
3.3 验证环境
bash
# 检查 Rust 版本(应显示 esp 工具链)
rustc --version
# 检查是否检测到开发板
probe-rs list
四、项目结构
ADC_电压/
├── .cargo/
│ └── config.toml # 编译目标 & 烧录器配置
├── .clippy.toml # Clippy 静态分析配置
├── .vscode/ # VS Code 编辑器配置
├── Cargo.toml # 依赖声明
├── rust-toolchain.toml # 指定 esp 工具链
├── build.rs # 链接脚本辅助(帮助定位链接错误)
├── docs/ # 参考文档
└── src/
├── lib.rs # 库入口(仅 #![no_std])
└── bin/
└── main.rs # 主程序:ADC 电压采集
4.1 关键配置文件
rust-toolchain.toml --- 指定 Espressif 定制工具链:
toml
[toolchain]
channel = "esp" # 使用 esp 通道(支持 Xtensa 架构)
.cargo/config.toml --- 编译目标 & 烧录配置:
toml
[target.xtensa-esp32s3-none-elf]
# probe-rs 烧录命令,烧录后自动运行
runner = "probe-rs run --chip=esp32s3 --preverify --always-print-stacktrace --no-location"
[build]
target = "xtensa-esp32s3-none-elf" # Xtensa 编译目标
[unstable]
build-std = ["alloc", "core"] # 从源码编译核心库(嵌入式必需)
Cargo.toml --- 核心依赖说明:
toml
[dependencies]
# ESP32-S3 硬件抽象层(unstable 特性启用实验性 API,如 ADC)
esp-hal = { version = "~1.1.0", features = ["esp32s3", "unstable"] }
# IDF bootloader 兼容层(必须,提供 esp_app_desc! 宏)
esp-bootloader-esp-idf = { version = "0.5.0", features = ["esp32s3"] }
# RTT 日志输出(通过 probe-rs 在电脑端查看)
rtt-target = "0.6.2"
# RTT panic 输出(程序崩溃时通过 RTT 打印错误信息)
panic-rtt-target = "0.2.0"
五、完整代码 & 逐行解析
5.1 src/bin/main.rs --- 完整代码
rust
//! ESP32-S3 ADC 模拟电压采集示例
//!
//! 功能:通过 GPIO1 读取模拟电压,经 ADC 转换后输出毫伏值。
//! 硬件:将待测电压(0~3.1V)接到 GPIO1 引脚即可。
//!
//! 关键概念:
//! - ADC(模数转换器):将连续的模拟电压信号转为离散的数字值。
//! - 校准(Calibration):消除芯片个体差异,提高测量精度。
//! - 衰减(Attenuation):扩大 ADC 可测量的输入电压范围。
//! _0dB → 0~1.1V (精度最高)
//! _2_5dB → 0~1.5V
//! _6dB → 0~2.2V
//! _11dB → 0~3.1V (范围最广,本例使用)
#![no_main]
#![no_std]
use esp_bootloader_esp_idf;
use esp_hal::{
analog::adc::{Adc, AdcCalCurve, AdcConfig, Attenuation},
delay::Delay,
main,
};
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};
// 声明 ESP-IDF 应用描述符(启动引导所需)
esp_bootloader_esp_idf::esp_app_desc!();
#[main]
fn main() -> ! {
// 初始化 RTT 打印通道(用于向电脑输出调试信息)
rtt_init_print!();
rprintln!("ESP32-S3 ADC 校准采集示例");
// 初始化外设(获取芯片所有外设的控制权)和延时器
let peripherals = esp_hal::init(esp_hal::Config::default());
let delay = Delay::new();
// ── 配置 ADC1 通道 ──────────────────────────────
let mut adc1_config = AdcConfig::new();
// 启用 GPIO1 的 ADC 功能,使用曲线校准,衰减设为 11dB(量程 0~3.1V)
let mut pin_gpio1 =
adc1_config.enable_pin_with_cal::<_, AdcCalCurve<_>>(peripherals.GPIO1, Attenuation::_11dB);
// 用配置好的参数创建 ADC1 实例
let mut adc1 = Adc::new(peripherals.ADC1, adc1_config);
// ── 循环采集 ─────────────────────────────────────
loop {
// 阻塞式读取:等待转换完成并返回校准后的毫伏值
let voltage_mv = adc1.read_blocking(&mut pin_gpio1);
// 同时输出毫伏和伏特两种单位,方便对照
rprintln!(
"GPIO1: {} mV ({:.3} V)",
voltage_mv,
voltage_mv as f32 / 1000.0
);
// 每 100ms 采样一次
delay.delay_millis(100);
}
}
5.2 src/lib.rs --- 库入口
rust
// 库入口:当前示例无需公共库代码,仅标记 no_std(嵌入式环境无标准库)
#![no_std]
这个文件是 Rust 库的入口。本示例所有逻辑都在
bin/main.rs中,lib.rs仅声明#![no_std],告诉编译器这是嵌入式项目。
六、代码逐段深度解析
6.1 文件头 doc 注释
rust
//! ESP32-S3 ADC 模拟电压采集示例
//!
//! 功能:通过 GPIO1 读取模拟电压,经 ADC 转换后输出毫伏值。
//! 硬件:将待测电压(0~3.1V)接到 GPIO1 引脚即可。
//! 是 Rust 的模块级文档注释 ,可以用 cargo doc 生成 HTML 文档。养成写文档注释的好习惯,代码可读性大幅提升。
6.2 嵌入式程序入口
rust
#![no_main]
#![no_std]
| 属性 | 含义 | 为什么需要 |
|---|---|---|
#![no_std] |
不链接标准库 std |
嵌入式无操作系统,std 依赖 OS 功能(文件、网络等) |
#![no_main] |
禁用 C 运行时的 main |
嵌入式程序由硬件启动,不是由 OS 调用 |
类比: 普通 Rust 程序像在 Windows/Mac 上运行的 App,有 OS 帮你管理一切。嵌入式程序像直接焊在电路板上的固件,没有 OS,一切自己来。
6.3 导入依赖
rust
use esp_hal::{
analog::adc::{Adc, AdcCalCurve, AdcConfig, Attenuation},
delay::Delay,
main,
};
| 导入项 | 作用 |
|---|---|
Adc |
ADC 外设驱动,执行实际的电压采集 |
AdcConfig |
ADC 配置器,用于启用引脚和设置参数 |
AdcCalCurve |
曲线校准算法,用多项式拟合消除芯片个体误差 |
Attenuation |
衰减档位枚举(_0dB / _2_5dB / _6dB / _11dB) |
Delay |
延时工具,提供 delay_millis() 等方法 |
main |
属性宏,标记程序入口函数 |
rust
use panic_rtt_target as _;
use rtt_target::{rprintln, rtt_init_print};
| 导入项 | 作用 |
|---|---|
panic_rtt_target |
panic 时通过 RTT 输出错误信息(as _ 表示只注册行为,不直接使用) |
rtt_init_print |
初始化 RTT 打印通道的宏 |
rprintln |
通过 RTT 输出一行文字(类似 println!,但走调试通道) |
6.4 初始化
rust
rtt_init_print!();
let peripherals = esp_hal::init(esp_hal::Config::default());
let delay = Delay::new();
执行流程:
rtt_init_print!() → 打开 RTT 调试通道
↓
esp_hal::init(default()) → 初始化芯片时钟、GPIO 等基础外设
↓ 返回 peripherals 结构体(所有外设的"所有权证书")
Delay::new() → 创建延时器(基于芯片时钟)
Rust 所有权概念:
peripherals包含了所有外设的控制权。每个外设(如GPIO1、ADC1)只能被取出一次,取出后就不能再被其他人使用------这在编译期就防止了外设冲突。
6.5 ADC 配置(核心)
rust
let mut adc1_config = AdcConfig::new();
let mut pin_gpio1 =
adc1_config.enable_pin_with_cal::<_, AdcCalCurve<_>>(peripherals.GPIO1, Attenuation::_11dB);
let mut adc1 = Adc::new(peripherals.ADC1, adc1_config);
逐步拆解:
第 1 步:创建配置器
rust
let mut adc1_config = AdcConfig::new();
AdcConfig 是一个"构建器",用来收集所有 ADC 通道的配置。
第 2 步:启用引脚 + 校准
rust
adc1_config.enable_pin_with_cal::<_, AdcCalCurve<_>>(peripherals.GPIO1, Attenuation::_11dB)
| 参数 | 含义 |
|---|---|
peripherals.GPIO1 |
将 GPIO1 从通用 GPIO "切换"为 ADC 模拟输入 |
Attenuation::_11dB |
衰减 11dB,量程 0~3.1V |
AdcCalCurve<_> |
使用曲线校准(芯片出厂时在 efuse 中烧录了校准参数) |
校准的意义: 每颗芯片的 ADC 都有微小差异。
AdcCalCurve会读取芯片 efuse 中的校准数据,用多项式拟合修正测量值,精度比未校准高 2~3 倍。
返回值: 一个 AdcPin 句柄,后续读取时需要传入。
第 3 步:创建 ADC 实例
rust
let mut adc1 = Adc::new(peripherals.ADC1, adc1_config);
用配置好的参数创建 ADC1 驱动实例。peripherals.ADC1 的所有权被转移进 Adc,此后只能通过 adc1 操作 ADC1。
6.6 循环采集
rust
loop {
let voltage_mv = adc1.read_blocking(&mut pin_gpio1);
rprintln!(
"GPIO1: {} mV ({:.3} V)",
voltage_mv,
voltage_mv as f32 / 1000.0
);
delay.delay_millis(100);
}
采集流程:
read_blocking(&mut pin_gpio1)
│
├─ 1. 启动 ADC 转换(模拟电压 → 数字值)
├─ 2. 等待转换完成(阻塞当前线程)
├─ 3. 读取 12 位原始值(0~4095)
├─ 4. 应用校准曲线修正
└─ 5. 返回校准后的毫伏值(u16,单位 mV)
例:1650 表示 1.650V
输出格式:
rust
rprintln!("GPIO1: {} mV ({:.3} V)", voltage_mv, voltage_mv as f32 / 1000.0);
// ^^^^^^ ^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
// 标签 毫伏值 转为伏特,保留 3 位小数
| 采样值 (mV) | 输出 | 含义 |
|---|---|---|
| 0 | GPIO1: 0 mV (0.000 V) |
接地 |
| 1650 | GPIO1: 1650 mV (1.650 V) |
约 3.3V 的一半 |
| 3100 | GPIO1: 3100 mV (3.100 V) |
11dB 衰减的满量程 |
七、Rust 嵌入式核心概念速查
7.1 #![no_std] vs std
普通 Rust 程序 嵌入式 Rust 程序
┌──────────────┐ ┌──────────────┐
│ 你的代码 │ │ 你的代码 │
├──────────────┤ ├──────────────┤
│ std │ ← 标准库 │ core │ ← 仅核心库
├──────────────┤ ├──────────────┤
│ 操作系统 │ │ 硬件 │
└──────────────┘ └──────────────┘
core 提供:基本类型、迭代器、Option、Result 等。
std 额外提供:文件 I/O、网络、线程、println! 等(依赖 OS)。
7.2 RTT 调试输出
RTT(Real-Time Transfer) 是 Segger 开发的调试协议,通过 USB-JTAG 传输数据,无需额外串口:
| 对比 | 串口(UART) | RTT |
|---|---|---|
| 速度 | 115200 bps | 几乎零延迟 |
| 占用引脚 | 需要 TX/RX | 通过 USB 直传 |
| CPU 开销 | 较高 | 极低 |
| 配置复杂度 | 需配置波特率 | rtt_init_print!() 一行搞定 |
7.3 esp-hal 的 ADC API 层次
┌─────────────────────────────────────────────────┐
│ AdcConfig + AdcCalCurve (配置层:选择引脚、衰减、校准) │
├─────────────────────────────────────────────────┤
│ Adc::read_blocking() (HAL 层:一行代码完成采集) │
├─────────────────────────────────────────────────┤
│ ESP32-S3 ADC 寄存器 (硬件层:实际的电信号转换) │
└─────────────────────────────────────────────────┘
esp-hal 帮你封装了底层寄存器操作,你只需关注:用哪个引脚、什么衰减、是否校准。
八、编译 & 烧录
8.1 编译
bash
cd examples/ADC_电压
# 编译(首次较慢,后续增量编译很快)
cargo build
编译成功输出:
Finished `dev` profile [optimized for size] target(s) in 25.37s
8.2 烧录 & 运行
bash
# 一条命令:编译 + 烧录 + 运行
cargo run
前提: 嘉立创开发板通过 USB 连接电脑,
probe-rs list能检测到芯片。
8.3 查看输出
烧录成功后,终端直接显示 RTT 输出:
Running `probe-rs run --chip=esp32s3 ...`
Erasing ✔ [00:00:01] [████████████████████] 128.00 KiB/128.00 KiB
Programming ✔ [00:00:02] [████████████████████] 126.42 KiB/126.42 KiB
Finished in 3.47s
ESP32-S3 ADC 校准采集示例
GPIO1: 1650 mV (1.650 V)
GPIO1: 1648 mV (1.648 V)
GPIO1: 3100 mV (3.100 V)
...(每 100ms 输出一行)
按 Ctrl+C 停止。
九、实验拓展
9.1 改变衰减档位
将 Attenuation::_11dB 改为其他档位,观察量程变化:
rust
// 高精度、低量程
adc1_config.enable_pin_with_cal::<_, AdcCalCurve<_>>(peripherals.GPIO1, Attenuation::_0dB);
// 量程 0~1.1V,精度最高
9.2 读取多个引脚
rust
let mut pin_gpio1 =
adc1_config.enable_pin_with_cal::<_, AdcCalCurve<_>>(peripherals.GPIO1, Attenuation::_11dB);
let mut pin_gpio2 =
adc1_config.enable_pin_with_cal::<_, AdcCalCurve<_>>(peripherals.GPIO2, Attenuation::_11dB);
let mut adc1 = Adc::new(peripherals.ADC1, adc1_config);
loop {
let v1 = adc1.read_blocking(&mut pin_gpio1);
let v2 = adc1.read_blocking(&mut pin_gpio2);
rprintln!("GPIO1: {} mV, GPIO2: {} mV", v1, v2);
delay.delay_millis(100);
}
9.3 用 ADC 读取电位器
最经典的 ADC 实验------旋转电位器(可变电阻):
3.3V ──── 电位器 ──── GND
│
GPIO1
旋转旋钮,电压在 0~3.3V 之间变化,程序实时输出当前电压。
十、常见问题
Q1:编译报错 can't find crate for core
原因: 未正确安装 Xtensa 工具链。
解决:
bash
espup install
source ~/.bashrc # 或 ~/.zshrc
Q2:烧录报错 No debug probe was found
原因: 开发板未连接或驱动问题。
解决:
- 检查 USB 线是否支持数据传输(非纯充电线)
- macOS 通常免驱;Windows 需安装 Zadig 或 WinUSB 驱动
- 运行
probe-rs list确认检测到芯片
Q3:读数一直为 0 或满量程
可能原因:
- 引脚接错:确认接的是 GPIO1,不是其他引脚
- 电压超范围:超过 3.1V(11dB 档)会饱和到最大值
- 未接 GND:待测电压源的地线必须与开发板 GND 相连
Q4:Wi-Fi 开启后 ADC 读数异常
原因: 使用了 ADC2 引脚(GPIO9~18),与 Wi-Fi 冲突。
解决: 改用 ADC1 引脚(GPIO1~GPIO8)。
Q5:RTT 输出乱码或无输出
解决:
- 确认
Cargo.toml中有rtt-target和panic-rtt-target依赖 - 确认代码中有
rtt_init_print!()宏调用 - 尝试重新烧录:
cargo run
十一、关键概念总结
| 概念 | 说明 |
|---|---|
| ADC | 模数转换器,将模拟电压转为数字值 |
| 分辨率 | 12 位 = 2¹² = 4096 级(0~4095) |
| 衰减 | 扩大输入量程,降低精度 |
| 校准 | 用 efuse 中的出厂参数修正测量误差 |
AdcCalCurve |
曲线校准,多项式拟合,精度最高 |
read_blocking() |
阻塞式读取,等转换完成再返回 |
#![no_std] |
嵌入式必须,不使用标准库 |
#![no_main] |
嵌入式必须,不使用 C 运行时 main |
| PAC | Peripheral Access Crate,寄存器级访问层 |
| HAL | Hardware Abstraction Layer,硬件抽象层 |
| RTT | Real-Time Transfer,通过 USB-JTAG 实时输出调试信息 |
| probe-rs | Rust 嵌入式烧录 & 调试工具 |
| esp-hal | Espressif 官方 Rust HAL 库 |
十二、参考资料
- esp-hal 官方仓库
- esp-hal ADC 示例
- esp-rs 嵌入式开发指南
- probe-rs 文档
- ESP32-S3 技术参考手册(PDF)
- ESP32-S3 数据手册
- 嘉立创开源硬件平台
- 本项目源码
作者:CXi
如有疑问,欢迎在 GitHub Issues 中交流。