ADC 模拟电压采集

【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 包含了所有外设的控制权。每个外设(如 GPIO1ADC1)只能被取出一次,取出后就不能再被其他人使用------这在编译期就防止了外设冲突。

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 提供:基本类型、迭代器、OptionResult 等。

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

原因: 开发板未连接或驱动问题。

解决:

  1. 检查 USB 线是否支持数据传输(非纯充电线)
  2. macOS 通常免驱;Windows 需安装 Zadig 或 WinUSB 驱动
  3. 运行 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 输出乱码或无输出

解决:

  1. 确认 Cargo.toml 中有 rtt-targetpanic-rtt-target 依赖
  2. 确认代码中有 rtt_init_print!() 宏调用
  3. 尝试重新烧录: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 库

十二、参考资料


作者:CXi

如有疑问,欢迎在 GitHub Issues 中交流。

相关推荐
IT方大同1 小时前
(嵌入式操作系统)信号量
嵌入式硬件·c#
codexu_4612291872 小时前
NoteGen 里一条记录如何变成 Markdown
前端·笔记·rust·tauri
意法半导体STM322 小时前
【官方原创】如何为STM32CubeMX2配置Visual Studio Code配置方案
vscode·stm32·单片机·嵌入式硬件·策略模式·stm32cubemx·嵌入式开发
Rust研习社2 小时前
Rust 错误处理的黄金搭档:一个定义错误,一个传播错误
后端·rust·编程语言
自小吃多2 小时前
IVD设备-以GB4793.1做安规摸底
笔记·嵌入式硬件
雾削木3 小时前
B语言经典教程现代化重构
java·前端·stm32·单片机·嵌入式硬件
Hello-FPGA3 小时前
Camera Link 与 CoaXPress 技术对比 如何选择你的相机接口
单片机·嵌入式硬件
Digitally3 小时前
如何快速将文件从电脑传输到平板电脑
stm32·嵌入式硬件·电脑
2601_958352903 小时前
嵌入式对讲收音降噪难题根治方案|AP-0316语音模组原理、实测与落地教程
人工智能·嵌入式硬件·语音识别·ai降噪·回音消除·音频处理模块