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 实际接线图
三、代码实战 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" #define ADC_CHANNEL ADC1_CHANNEL_6 #define ADC_WIDTH ADC_WIDTH_BIT_12 #define ADC_ATTEN ADC_ATTEN_DB_11 static esp_adc_cal_characteristics_t adc_chars;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 ); printf ("ADC初始化完成!\n" ); } void app_main (void ) { adc_init(); printf ("开始读取电位器值...\n" ); printf ("旋转电位器,观察数值变化\n\n" ); while (1 ) { int raw_value = adc1_get_raw(ADC_CHANNEL); 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 )); } }
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);
可选值:
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 ); } 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" #define ADC_CHANNEL ADC1_CHANNEL_6 #define ADC_WIDTH ADC_WIDTH_BIT_12 #define ADC_ATTEN ADC_ATTEN_DB_11 #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_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_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 ) { int raw = adc1_get_raw(ADC_CHANNEL); uint32_t voltage = esp_adc_cal_raw_to_voltage(raw, &adc_chars); 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 ; 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对噪声敏感,特别是电源噪声
解决方案 :
硬件滤波 :在ADC输入引脚并联100nF陶瓷电容
软件滤波 :多次采样取平均(见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 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 #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); 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显示屏和各种传感器!
参考资源
📌 课后作业 :
修改代码,实现”光线感应灯”——光线暗时LED自动变亮
尝试用ADC读取热敏电阻,做一个简易温度计
思考:如果要测量5V电压,应该如何设计分压电路?