跳到主要内容

模拟输入

本节将介绍 ADC(模数转换器)的基本概念,并通过读取电位器的电压值,讲解如何在 ESP32 MicroPython 环境中读取模拟信号。

1. 模拟信号

模拟信号是一种在一定范围内可以连续变化的信号。

比如调光灯旋钮,可以平滑地调节灯的亮度,从完全熄灭到最亮,中间有无数个亮度级别。这个亮度调节的过程就是模拟的。而数字信号就像普通电灯开关,只有“开”和“关”。

现实世界中,像温度、光线强度、声音大小等许多物理量都是模拟的,它们的变化是连续而平滑的。

对于 ESP32,若要测量连续变化的信号(例如读取电位器、传感器的数值),仅用 1 (High) 或 0 (Low) 两种数字状态是无法实现的。此时,就需要使用 ADC

  • ADC(Analog-to-Digital Converter, 模数转换器):一种能将连续的模拟电压信号(例如 0V ~ 3.3V)转换为 ESP32 可处理的数字值的设备。

    ADC

简单来说,ADC 就像一把将 0~3.3V 之间的电压切分成很多“刻度”的尺子,让每个电压范围都对应一个具体的数字。ADC 能将电压细分成多少个等级,这个能力就叫做分辨率。分辨率越高,能检测的电压变化就越细致。

ESP32 的 ADC 通常是 12 位的,也就是一共可以分成 2¹²(= 4096) 个等级。因此,ADC 的读数范围是 0 ~ 4095

  • 输入电压为 0V,ADC 读数约等于 0
  • 当输入电压从 0V 连续变化到 3.3V 时,ADC 读数会相应地从 0 连续变化到 4095
ADC

这样,MicroPython 程序就可以通过 adc.read() 得到 0~4095 之间的整数,这个数值直接对应了输入引脚上的电压大小。

2. ADC 引脚

并不是所有 ESP32 引脚都支持模拟输入。需要查询对应开发板的引脚图或者芯片的手册,查找标有“ADC”的引脚作为模拟输入使用。
建议优先选择 ADC1 通道的引脚,以避免与其他功能冲突。

芯片型号ADC 通道 1 (建议使用)ADC 通道 2参考文档
ESP32GPIO32 - GPIO39GPIO0, 2, 4, 12-15, 25-27ESP32 技术规格书 2.2 节
ESP32-C3GPIO0 - GPIO5-ESP32-C3 技术规格书 2.3.2 节
ESP32-C6GPIO0 - GPIO6-ESP32-C6 技术规格书 2.3.3 节
ESP32-C5GPIO1 - GPIO6-ESP32-C5 技术规格书 2.3.3 节
ESP32-S3GPIO1 - GPIO10GPIO11 - GPIO20ESP32-S3 技术规格书 2.3.3 节
ESP32-P4GPIO16 - GPIO23GPIO49 - GPIO54ESP32-P4 技术规格书 2.3.3 节
其它--乐鑫文档中心(CDP)

3. 搭建电路

需要使用的器件有:

  • 电位器 * 1
  • 面包板 * 1
  • 导线
  • ESP32 开发板
ESP32-S3-Zero 引脚图

ESP32-S3-Zero-Pinout

接线图

电路解析

让我们理解一下这个模拟信号读取电路是如何工作的:

  1. 电位器的连接:

    • VCC 引脚:连接到 ESP32 的 3.3V,提供电位器工作电压。
    • GND 引脚:连接到 ESP32 的 GND,形成电路回路。
    • 信号引脚(中间):连接到 ESP32 的 GPIO7(ADC 引脚),输出 0V~3.3V 之间的模拟电压。
  2. 电位器工作原理:

    • 电位器内部是一个可变电阻,通过旋转可以改变阻值。
    • 旋转电位器旋钮时,信号引脚的输出电压会在 0V 到 3.3V 之间连续变化。
    • 完全逆时针:输出接近 0V。
    • 完全顺时针:输出接近 3.3V。

4. 代码

4.1 REPL 交互

在编写完整程序前,可通过 REPL 熟悉 ADC 相关函数。

在 Shell 中逐行输入以下命令并观察现象:

from machine import Pin, ADC
pot = ADC(Pin(7))          # 在 GPIO7 上创建 ADC 对象
pot.read()                 # 直接读取,此时数值应反映当前电位器位置 (0-4095)
pot.read()                 # 再次读取
pot.read_uv()              # 读取电压值,单位微伏 (uV)

4.2 完整代码示例

在 Thonny IDE 中新建文件,输入并运行以下代码。此代码将循环读取电压值,并按特定格式输出,以便配合 Thonny 的“绘图仪”功能使用。

import time
from machine import Pin, ADC

# 定义电位器连接的引脚 (GPIO 7)
POT_PIN = 7

# 初始化 ADC
# 1. 创建 ADC 对象关联引脚
pot = ADC(Pin(POT_PIN))

while True:
# 读取原始模拟值 (0 - 4095)
adc_value = pot.read()

# 读取电压值 (单位:微伏 uV),并转换为毫伏 (mV)
voltage_uv = pot.read_uv()
voltage_mv = voltage_uv / 1000

# 格式化输出,方便在绘图仪中观察
# 格式:Label:Value
print("ADC:", adc_value, ",Voltage_mV:", voltage_mv)

# 延时 0.1 秒
time.sleep(0.1)

运行结果:

将代码运行在 ESP32 开发板上后,Shell 窗口将持续输出数值。

在 Thonny IDE 中,点击菜单栏的 “视图 (View)” -> “绘图仪 (Plotter)”,即可看到右侧出现实时的波形图。转动电位器,曲线会随之升降。

运行结果

深入一点:为什么最大读数不正好对应 3.3V?

可能会观察到,ADC 的读数在电压未达到 3.3V 时就已饱和(达到 4095),或在两端(接近 0V 和 3.3V)表现出非线性。

此为 ESP32 ADC 的设计特性之一。其内部核心电路能直接处理的电压范围有限,因此使用名为衰减器 (Attenuator) 的内部模块扩展可测量电压范围。

在 MicroPython 环境下,固件默认会启用一个能测量最高电压的衰减选项。根据官方文档,此设置下 ESP32 S3 ADC 可靠测量上限约为 3.1V。(注意:不同芯片在不同的衰减选项下,可测量的输入电压范围是不同的。查看 此表。)

因此,当输入电压超过 3.1V 时,读数就会“饱和”,即保持在最大值 4095。

因此,read_uv() 是更推荐的方式,它会利用出厂校准数据将原始读数转换为较为准确的电压值(微伏),在一定程度上补偿了非线性和参考电压的误差。

代码解析

  1. pot = ADC(Pin(POT_PIN))

    创建一个 ADC 对象来控制指定的 GPIO 引脚。

  2. pot.read()

    读取 ADC 的原始数值。对于 ESP32,这通常是一个 12 位的数值,范围是 0 ~ 4095。

  3. pot.read_uv()

    直接返回以微伏 (μV) 为单位的校准电压值。这是一个非常实用的函数,不需要手动进行 (读数 / 4095) * 3300 的数学运算,且通常经过了出厂校准,精度更高。

  4. print("ADC:", adc_value, ",Voltage_mV:", voltage_mv)

    Thonny 绘图仪格式说明: Thonny 的绘图仪通过识别 Shell 中的打印输出来绘制曲线。为了让绘图仪正确显示数据,建议遵循以下格式:

    • 纯数值:每行打印一个或多个数值(用逗号或空格分隔)。
    • 键值对(推荐):使用 名称:数值 的格式。例如 print("ADC:", value)

    这种格式不仅能绘制曲线,还能在图例中显示每条曲线的名称,方便区分多组数据。

5. 拓展:减少噪声

当停止转动电位器时,可能会观察到 ADC 读数并未稳定停留在某一个值,而是在小范围内持续跳动,有时甚至出现较大毛刺。

ADC 噪声

这种现象通常是由噪声引起的。ESP32 的 ADC 对电源噪声和外部环境的电磁干扰比较敏感。

要减少噪声的影响,通常有两种方法:

  • 硬件滤波:在 ADC 输入引脚和 GND 之间并联一个 0.1µF (100nF) 的陶瓷电容,滤除高频干扰。

  • 软件滤波:通过算法对多次采样结果进行处理。最简单有效的方法是均值滤波。

代码示例:去极值均值滤波

以下代码演示了如何连续采样多次,去除最大值和最小值后取平均,从而获得平滑的读数。

import time
from machine import Pin, ADC

POT_PIN = 7

pot = ADC(Pin(POT_PIN))

def read_average_adc(adc_obj, times=10):
"""
连续读取多次 ADC 值,去掉最大值和最小值后计算平均数
:param adc_obj: ADC 对象
:param times: 采样次数,默认为 10 次
:return: 平均后的整数值
"""
val_list = []
for _ in range(times):
val_list.append(adc_obj.read())
time.sleep_ms(1) # 采样间隔

# 去掉最大值和最小值,计算剩余数据的平均值
if len(val_list) > 2:
val_list.remove(min(val_list))
val_list.remove(max(val_list))

return int(sum(val_list) / len(val_list))

while True:
# 获取 20 次采样的平均值
smooth_value = read_average_adc(pot, 20)

print("Raw:", pot.read(), "Smooth:", smooth_value)
time.sleep(0.1)

应用软件滤波后,可以看到波形中的毛刺减少,曲线更加平滑。

均值滤波输出

6. 相关链接