ESP32-S3 IDF编程第八课:ADC模拟输入 —— 让单片机”看见”真实世界

上节课我们学会了任务间的”悄悄话”,这节课让ESP32-S3睁开”模拟眼睛”——ADC,从此数字世界与模拟世界不再绝缘。

你可能会问:GPIO不是能读高低电平吗?为什么还要学ADC?

来,看个真实场景:

  • 你想做个光线感应灯,光敏电阻的阻值随光线变化,但GPIO只能判断”亮/暗”两种状态
  • 你想做个温度监测仪,热敏电阻的电压随温度变化,但GPIO读不出具体温度值
  • 你想做个电池电量显示,锂电池电压从4.2V降到3.0V,但GPIO只能判断”有电/没电”

这时候,你就需要ADC(Analog-to-Digital Converter,模数转换器)——它能把03.3V之间的任意电压,转换成04095之间的数字值。就像一把精密的尺子,把模拟世界切成4096个刻度!


一、ADC是什么?

1.1 数字 vs 模拟

想象你调节台灯的亮度:

  • 数字方式:只有”开”和”关”两档(GPIO的高低电平)
  • 模拟方式:可以平滑地从最暗调到最亮,中间有无数个亮度级别

现实世界中,温度、光线、声音、压力都是模拟信号,它们的变化是连续而平滑的。但单片机只认识数字(0和1),所以需要ADC来做”翻译”。

1.2 ESP32-S3的ADC规格

ESP32-S3内置了2个12位SAR ADC,堪称豪华配置:

参数 数值
ADC数量 2个(ADC1 + ADC2)
分辨率 12位(可配置9/10/11/12位)
通道数 20个(ADC1有10个,ADC2有10个)
采样范围 0 ~ 3.3V(通过衰减可扩展)
数字输出 0 ~ 4095(12位模式下)

12位分辨率意味着什么?

  • 把3.3V电压分成 2^12 = 4096 个等级
  • 每个等级代表 3.3V / 4096 ≈ 0.8mV 的电压变化
  • 也就是说,ADC能分辨出0.8mV的电压差异!

1.3 ADC通道与GPIO对应关系

ADC1通道(推荐使用,不受WiFi影响):

通道 GPIO 备注
ADC1_CHANNEL_0 GPIO1 ✅ 推荐
ADC1_CHANNEL_1 GPIO2 ✅ 推荐
ADC1_CHANNEL_2 GPIO3 ✅ 推荐
ADC1_CHANNEL_3 GPIO4 ✅ 推荐
ADC1_CHANNEL_4 GPIO5 ✅ 推荐
ADC1_CHANNEL_5 GPIO6 ✅ 推荐
ADC1_CHANNEL_6 GPIO7 ✅ 推荐
ADC1_CHANNEL_7 GPIO8 ✅ 推荐
ADC1_CHANNEL_8 GPIO9 ✅ 推荐
ADC1_CHANNEL_9 GPIO10 ✅ 推荐

ADC2通道(⚠️ 使用WiFi时会冲突):

通道 GPIO 备注
ADC2_CHANNEL_0 GPIO11 ⚠️ WiFi占用
ADC2_CHANNEL_1 GPIO12 ⚠️ WiFi占用

💡 建议:优先使用ADC1的通道,避免与WiFi功能冲突!


二、硬件准备

2.1 所需材料

  • ESP32-S3开发板 × 1
  • 电位器(10kΩ)× 1
  • 光敏电阻(5528)× 1(可选)
  • 面包板 × 1
  • 杜邦线若干

2.2 电位器接线图

电位器有3个引脚,这样接:

1
2
3
4
5
6
7
8
9
10
11
12
13
    3.3V


┌───────────┐
│ 电位器 │
│ ┌───┐ │
└───┤ ├───┘
└───┘


GPIO7 (ADC1_CH6)

GND ◄──┘

接线说明:

  • 电位器两边引脚:一边接3.3V,一边接GND
  • 电位器中间引脚:接GPIO7(输出0~3.3V可变电压)

🔧 原理:旋转电位器旋钮时,中间引脚输出的电压会在0V到3.3V之间连续变化,完全逆时针≈0V,完全顺时针≈3.3V。

2.3 实际接线图

ESP32 ADC接线图


三、代码实战

3.1 基础代码:读取电位器值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/adc.h"
#include "esp_adc_cal.h"

// ADC配置
#define ADC_CHANNEL ADC1_CHANNEL_6 // GPIO7
#define ADC_WIDTH ADC_WIDTH_BIT_12 // 12位分辨率
#define ADC_ATTEN ADC_ATTEN_DB_11 // 11dB衰减,量程0-3.3V

// 校准特性结构体
static esp_adc_cal_characteristics_t adc_chars;

void adc_init(void)
{
// 1. 配置ADC1的分辨率
adc1_config_width(ADC_WIDTH);

// 2. 配置通道衰减(决定测量电压范围)
adc1_config_channel_atten(ADC_CHANNEL, ADC_ATTEN);

// 3. 校准ADC(提高精度)
esp_adc_cal_characterize(
ADC_UNIT_1, // ADC单元
ADC_ATTEN, // 衰减
ADC_WIDTH, // 位宽
DEFAULT_VREF, // 默认参考电压1100mV
&adc_chars // 校准特性
);

printf("ADC初始化完成!\n");
}

void app_main(void)
{
// 初始化ADC
adc_init();

printf("开始读取电位器值...\n");
printf("旋转电位器,观察数值变化\n\n");

while (1) {
// 读取原始ADC值(0-4095)
int raw_value = adc1_get_raw(ADC_CHANNEL);

// 转换为实际电压值(mV)
uint32_t voltage = esp_adc_cal_raw_to_voltage(raw_value, &adc_chars);

// 打印结果
printf("原始值: %4d | 电压: %4lu mV (%.2f V)\n",
raw_value, voltage, voltage / 1000.0);

vTaskDelay(pdMS_TO_TICKS(200)); // 200ms刷新一次
}
}

3.2 代码解析

3.2.1 衰减配置(Attenuation)

衰减决定了ADC能测量的电压范围:

衰减 宏定义 测量范围 适用场景
0dB ADC_ATTEN_DB_0 0 ~ 950mV 小信号测量
2.5dB ADC_ATTEN_DB_2_5 0 ~ 1250mV 传感器信号
6dB ADC_ATTEN_DB_6 0 ~ 1750mV 中等电压
11dB ADC_ATTEN_DB_11 0 ~ 3100mV 3.3V系统

💡 一般选择11dB,因为ESP32-S3是3.3V系统,可以测量完整的0~3.3V范围。

3.2.2 分辨率配置

1
adc1_config_width(ADC_WIDTH_BIT_12);  // 12位分辨率

可选值:

  • ADC_WIDTH_BIT_9 → 0~511(9位)
  • ADC_WIDTH_BIT_10 → 0~1023(10位)
  • ADC_WIDTH_BIT_11 → 0~2047(11位)
  • ADC_WIDTH_BIT_12 → 0~4095(12位,推荐)

3.2.3 ADC校准

ESP32-S3的芯片之间存在差异,参考电压可能在1000mV~1200mV之间波动。校准可以:

  • 修正参考电压偏差
  • 修正ADC转换的线性误差
  • 让读数更接近真实电压值

3.3 进阶代码:平滑滤波

ADC读数会有噪声,用多次采样取平均来平滑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define SAMPLE_COUNT    64      // 采样次数

uint32_t read_adc_smooth(void)
{
uint32_t sum = 0;

// 多次采样
for (int i = 0; i < SAMPLE_COUNT; i++) {
sum += adc1_get_raw(ADC_CHANNEL);
esp_rom_delay_us(100); // 100us延时,避免采样过快
}

// 取平均
uint32_t average = sum / SAMPLE_COUNT;

return average;
}

3.4 实战项目:简易电压表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/adc.h"
#include "esp_adc_cal.h"
#include "driver/ledc.h"

// ADC配置
#define ADC_CHANNEL ADC1_CHANNEL_6
#define ADC_WIDTH ADC_WIDTH_BIT_12
#define ADC_ATTEN ADC_ATTEN_DB_11

// LED PWM配置(用亮度指示电压)
#define LED_GPIO GPIO5
#define LEDC_CHANNEL LEDC_CHANNEL_0
#define LEDC_TIMER LEDC_TIMER_0

static esp_adc_cal_characteristics_t adc_chars;

void pwm_init(void)
{
// 配置LEDC定时器
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.timer_num = LEDC_TIMER,
.duty_resolution = LEDC_TIMER_8_BIT,
.freq_hz = 5000,
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&ledc_timer);

// 配置LEDC通道
ledc_channel_config_t ledc_channel = {
.gpio_num = LED_GPIO,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = LEDC_CHANNEL,
.timer_sel = LEDC_TIMER,
.duty = 0,
.hpoint = 0
};
ledc_channel_config(&ledc_channel);
}

void adc_init(void)
{
adc1_config_width(ADC_WIDTH);
adc1_config_channel_atten(ADC_CHANNEL, ADC_ATTEN);
esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN, ADC_WIDTH, DEFAULT_VREF, &adc_chars);
}

void app_main(void)
{
adc_init();
pwm_init();

printf("简易电压表启动!\n");
printf("电位器控制LED亮度 + 串口显示电压\n\n");

while (1) {
// 读取ADC值
int raw = adc1_get_raw(ADC_CHANNEL);
uint32_t voltage = esp_adc_cal_raw_to_voltage(raw, &adc_chars);

// 将ADC值映射到PWM占空比(0-255)
uint32_t duty = (raw * 255) / 4095;
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL, duty);
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL);

// 显示电压表效果
int bars = (raw * 20) / 4095; // 20格进度条
printf("[%3lu mV] ", voltage);
for (int i = 0; i < 20; i++) {
printf(i < bars ? "█" : "░");
}
printf(" %2d%%\n", (raw * 100) / 4095);

vTaskDelay(pdMS_TO_TICKS(100));
}
}

运行效果:

1
2
3
4
5
6
7
8
简易电压表启动!
电位器控制LED亮度 + 串口显示电压

[ 50 mV] █░░░░░░░░░░░░░░░░░░░ 1%
[ 825 mV] ████████░░░░░░░░░░░░ 25%
[1650 mV] ███████████████░░░░░ 50%
[2475 mV] ███████████████████░ 75%
[3100 mV] ████████████████████ 95%

四、常见问题与解决

4.1 读数不稳定/跳变

原因:ADC对噪声敏感,特别是电源噪声

解决方案

  1. 硬件滤波:在ADC输入引脚并联100nF陶瓷电容
  2. 软件滤波:多次采样取平均(见3.3节)
  3. 电源优化:确保3.3V电源稳定,加去耦电容
1
2
3
4
5
GPIO7 ──┬── 电位器中间脚

=== 100nF

GND

4.2 读数非线性

现象:旋转电位器时,读数变化不均匀

原因

  • ESP32-S3的ADC在两端(接近0V和3.3V)存在非线性
  • 这是SAR ADC的固有特性

解决方案

  • 避开两端区域,使用中间80%的量程
  • 或使用校准表进行软件修正

4.3 ADC2读取失败

现象:使用ADC2时,读数返回错误

原因:ADC2与WiFi共享硬件资源

解决方案

  • 优先使用ADC1
  • 如果必须用ADC2,确保WiFi未启动

4.4 最大读数不到4095

现象:电位器拧到最大,读数只有4000左右

原因

  • 电位器本身有接触电阻
  • ADC参考电压不是精确的3.3V

解决方案

  • 这是正常现象,用校准后的电压值更准确
  • 或使用esp_adc_cal_raw_to_voltage()获取真实电压

五、拓展应用

5.1 光敏电阻测光

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 光敏电阻 + 10kΩ电阻分压
// 光越强,光敏电阻阻值越小,ADC读数越低

void light_sensor_task(void *pvParam)
{
while (1) {
int raw = adc1_get_raw(ADC_CHANNEL);

// 映射到亮度等级
if (raw < 1000) {
printf("光线:强 ☀️☀️☀️\n");
} else if (raw < 2500) {
printf("光线:中 ☀️☀️\n");
} else {
printf("光线:弱 ☀️\n");
}

vTaskDelay(pdMS_TO_TICKS(1000));
}
}

5.2 电池电压监测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 锂电池电压监测(4.2V满电 -> 3.0V欠压)
// 需要电阻分压:4.2V -> 3.3V以下

#define BAT_ADC_CHANNEL ADC1_CHANNEL_7
#define BAT_ATTEN ADC_ATTEN_DB_11

float read_battery_voltage(void)
{
int raw = adc1_get_raw(BAT_ADC_CHANNEL);
uint32_t adc_voltage = esp_adc_cal_raw_to_voltage(raw, &adc_chars);

// 分压比 = (R1 + R2) / R2 = (100k + 100k) / 100k = 2
float battery_voltage = (adc_voltage / 1000.0) * 2.0;

return battery_voltage;
}

5.3 多路ADC扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 同时监测多个传感器
adc1_channel_t channels[] = {
ADC1_CHANNEL_4, // 温度传感器
ADC1_CHANNEL_5, // 湿度传感器
ADC1_CHANNEL_6, // 光敏电阻
ADC1_CHANNEL_7 // 电池电压
};

const char* channel_names[] = {"温度", "湿度", "光照", "电池"};

void multi_adc_scan(void)
{
for (int i = 0; i < 4; i++) {
int raw = adc1_get_raw(channels[i]);
uint32_t voltage = esp_adc_cal_raw_to_voltage(raw, &adc_chars);
printf("%s: %lu mV ", channel_names[i], voltage);
}
printf("\n");
}

六、总结

这节课我们学会了:

ADC原理:12位分辨率,04095对应03.3V
通道选择:优先使用ADC1(GPIO1~GPIO10)
衰减配置:11dB衰减覆盖完整3.3V量程
校准技术:使用esp_adc_cal提高精度
滤波技巧:多次采样取平均降低噪声
实战项目:电位器控制LED亮度的电压表

下节课预告:ESP32-S3 IDF编程第九课:I2C通信 —— 连接传感器世界的”标准语言”,我们将学习I2C协议,驱动OLED显示屏和各种传感器!


参考资源


📌 课后作业

  1. 修改代码,实现”光线感应灯”——光线暗时LED自动变亮
  2. 尝试用ADC读取热敏电阻,做一个简易温度计
  3. 思考:如果要测量5V电压,应该如何设计分压电路?