4. 模拟信号采集:ADC原理、ESP32的ADC通道、电压读取、模拟值映射

好,咱们进入第四章。前面几章我们一直在跟数字信号打交道——GPIO要么高要么低,I2C和SPI传的都是0和1。但现实世界不是这样的。温度、光照、声音、压力……这些物理量都是连续变化的,也就是模拟量。

ESP32要感知这些模拟量,靠的就是ADC——模数转换器。这一章,我就带你彻底搞懂ESP32的ADC怎么用。

4.1 ADC原理:从连续到离散

ADC的全称是Analog-to-Digital Converter。它的工作说白了就三步:

  1. 采样:在某个时间点“拍一张照”,把模拟电压值固定下来。
  2. 量化:把连续的电压范围分成若干等级,每个等级对应一个数字码。
  3. 编码:把这个数字码转成二进制,输出给CPU。

举个例子。假设ADC的参考电压是0~3.3V,分辨率是12位。12位意味着什么?2的12次方等于4096个等级。也就是说,3.3V被分成了4096份,每份大约是0.8mV。

关键公式:

分辨率 = 参考电压 / 2^n

n = ADC位数。ESP32的ADC默认12位,所以分辨率 = 3.3V / 4096 ≈ 0.8mV/步

你想想看,如果传感器输出变化1mV,ADC就能检测到。这在大多数物联网场景下完全够用。

我在一个水质监测项目里用过10位的ADC,分辨率只有3.2mV,测pH值的时候误差大得离谱。后来换成12位,数据一下就稳了。所以,位数直接决定了你能测多细。

4.2 ESP32的ADC通道

ESP32有两个ADC模块:ADC1和ADC2。它们各有各的通道,但有个坑——ADC2在某些情况下会被Wi-Fi占用。

ADC模块 可用通道 GPIO引脚 注意事项
ADC1 CH0 ~ CH7 GPIO36, 39, 34, 35, 32, 33, 25, 26 Wi-Fi开启时不受影响,推荐使用
ADC2 CH0 ~ CH9 GPIO4, 0, 2, 15, 13, 12, 14, 27, 25, 26 Wi-Fi开启时部分通道不可用

避坑指南:我曾经在一个项目里用了ADC2的GPIO2,结果Wi-Fi一连接,ADC读数直接飘到天上去。查了两天才发现是ADC2被Wi-Fi占用了。所以,只要你的项目用Wi-Fi,老老实实用ADC1。

另外注意,ADC1的通道0~3(GPIO36, 39, 34, 35)是输入专用引脚,没有内部上拉/下拉。这意味着它们只能做模拟输入,不能当普通GPIO用。我习惯把传感器都接在这几个引脚上,省心。

4.3 电压读取:从原始值到真实电压

好,理论说完了,咱们直接上代码。ESP32用Arduino框架读取ADC,其实就一行:

int rawValue = analogRead(36);  // 读取GPIO36的ADC原始值,范围0~4095

但rawValue是0~4095的整数,不是电压。要转成电压,得做个简单的映射:

float voltage = rawValue * (3.3 / 4095.0);

等等,这里有个细节。为什么分母是4095而不是4096?因为ADC的原始值范围是0~4095,一共4096个值。但最大值4095对应的是参考电压3.3V。所以计算时用4095。

个人习惯:我一般把电压计算封装成一个函数,这样代码干净,也方便以后改参考电压。

float readADCVoltage(uint8_t pin) {
  int raw = analogRead(pin);
  return raw * (3.3 / 4095.0);
}

调用的时候:

float v = readADCVoltage(36);
Serial.printf("ADC电压: %.3f V\n", v);

嗯,这里要注意。ESP32的ADC其实不是特别线性,尤其是在接近0V和接近3.3V的两端。我做过测试,中间段(0.2V~3.1V)还算准,但两端误差可能达到2%~3%。如果你的应用对精度要求高,建议做两点校准——用万用表测实际电压,然后修正代码里的比例系数。

4.4 模拟值映射:从电压到物理量

拿到电压只是第一步。我们真正关心的是传感器对应的物理量——温度多少度?光照多强?距离多远?

这就涉及到模拟值映射。说白了,就是找到电压和物理量之间的对应关系。

举个例子。我用一个TMP36温度传感器,它的输出是10mV/°C,0°C时输出0.5V。那么:

float temperature = (voltage - 0.5) * 100.0;  // 单位°C

再比如一个光敏电阻模块,它的输出电压和光照强度成反比。但通常没有线性公式,得靠查表或拟合曲线。

我常用的映射方式有三种:

映射方式 适用场景 示例
线性映射 传感器输出与物理量成线性关系 TMP36温度传感器
查表映射 非线性但已知曲线 NTC热敏电阻
公式映射 有明确的数学关系 分压电路计算电阻值

对于查表映射,我一般把数据点存在一个数组里,然后用插值法算出中间值。代码大概长这样:

// 假设有10个校准点
float adcValues[10] = {0.1, 0.3, 0.5, 0.7, 0.9, 1.1, 1.3, 1.5, 1.7, 1.9};
float tempValues[10] = {85, 70, 55, 40, 25, 10, -5, -20, -35, -50};

float mapToTemperature(float voltage) {
  // 简单线性插值
  for (int i = 0; i < 9; i++) {
    if (voltage >= adcValues[i] && voltage <= adcValues[i+1]) {
      float ratio = (voltage - adcValues[i]) / (adcValues[i+1] - adcValues[i]);
      return tempValues[i] + ratio * (tempValues[i+1] - tempValues[i]);
    }
  }
  return -999;  // 超出范围
}

你看,代码不复杂,但效果很好。我在一个冷链监控项目里用这个办法,温度误差控制在±0.5°C以内。

4.5 本章知识结构

下面这张图把ADC的完整流程串起来了。从模拟信号到数字值,再到物理量,每一步都有对应的操作。

ADC信号采集与映射流程 模拟信号 传感器输出 ADC采样 analogRead() 原始值 0 ~ 4095 电压值 0 ~ 3.3V 模拟值映射 线性 / 查表 / 公式 物理量 温度 / 光照 / 距离 关键参数 • 分辨率:3.3V / 4096 ≈ 0.8mV • 推荐使用ADC1(GPIO36/39/34/35) • 两端非线性,建议中间段使用 • 高精度场景需两点校准 • 映射前先确认传感器数据手册

4.6 完整示例:读取光敏电阻

最后,我给你一个完整的实战例子。用光敏电阻模块检测环境光照,把ADC值映射成“亮/暗”等级。

#define LIGHT_SENSOR_PIN 34  // ADC1通道2

void setup() {
  Serial.begin(115200);
  pinMode(LIGHT_SENSOR_PIN, INPUT);
}

void loop() {
  int raw = analogRead(LIGHT_SENSOR_PIN);
  float voltage = raw * (3.3 / 4095.0);

  // 映射到0~100的亮度百分比
  // 假设暗处电压高(2.5V+),亮处电压低(0.5V-)
  int brightness = map(raw, 500, 3500, 100, 0);
  brightness = constrain(brightness, 0, 100);

  Serial.printf("原始值: %d | 电压: %.2fV | 亮度: %d%%\n", raw, voltage, brightness);

  if (brightness < 20) {
    Serial.println("→ 环境很暗,建议开灯");
  } else if (brightness > 80) {
    Serial.println("→ 环境很亮,注意遮光");
  }

  delay(1000);
}

这里用了Arduino的map()函数,它本质就是线性映射。但注意,map()只做整数运算,如果你需要浮点精度,还是自己写公式更靠谱。

一个小技巧:调试ADC的时候,我习惯先打印原始值和电压,用串口绘图器(Serial Plotter)看波形。这样能快速发现传感器是不是接反了、电源有没有纹波。别问我怎么知道的……