4. 模拟信号采集:ADC原理、ESP32的ADC通道、电压读取、模拟值映射
好,咱们进入第四章。前面几章我们一直在跟数字信号打交道——GPIO要么高要么低,I2C和SPI传的都是0和1。但现实世界不是这样的。温度、光照、声音、压力……这些物理量都是连续变化的,也就是模拟量。
ESP32要感知这些模拟量,靠的就是ADC——模数转换器。这一章,我就带你彻底搞懂ESP32的ADC怎么用。
4.1 ADC原理:从连续到离散
ADC的全称是Analog-to-Digital Converter。它的工作说白了就三步:
- 采样:在某个时间点“拍一张照”,把模拟电压值固定下来。
- 量化:把连续的电压范围分成若干等级,每个等级对应一个数字码。
- 编码:把这个数字码转成二进制,输出给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的完整流程串起来了。从模拟信号到数字值,再到物理量,每一步都有对应的操作。
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)看波形。这样能快速发现传感器是不是接反了、电源有没有纹波。别问我怎么知道的……