第6章 ESP32 ADC 類比輸入完整教學:analogRead、可變電阻、光敏電阻與電壓量測

發布日期: |更新日期:
文章主圖:第6章 ESP32 ADC 類比輸入完整教學:analogRead、可變電阻、光敏電阻與電壓量測

前言:ESP32 如何讀取「不是只有 HIGH / LOW」的訊號?

前面的 ESP32 GPIO 教學中,我們主要處理:

HIGH
LOW

這種數位訊號。

例如:

digitalRead(BUTTON_PIN);

讀到的基本上就是:

HIGH

或:

LOW

但是現實世界中的很多感測器並不是只有兩種狀態。

例如:

可變電阻
光敏電阻
類比溫度感測器
壓力感測器
搖桿
電池電壓
光線強弱
電流感測器
氣體感測器

輸出的可能是一段連續變化的電壓。

例如:

0.3V
0.8V
1.2V
1.7V
2.3V
2.8V

ESP32 要怎麼知道這些電壓是多少?

答案就是:

ADC

ADC 全名:

Analog-to-Digital Converter

中文稱為:

類比數位轉換器

ESP32 的 ADC 可以把輸入的類比電壓轉換成數位數值,讓程式可以進一步進行:

顯示
判斷
計算
記錄
Wi-Fi 傳送
PWM 控制
警報
自動控制

Arduino-ESP32 官方目前提供 analogRead() 進行單次 ADC 轉換,也提供 analogReadMilliVolts() 取得經校準換算後的毫伏值。

本章將完成:

ADC 原理
↓
12-bit
↓
0~4095
↓
analogRead()
↓
ADC1 / ADC2
↓
Wi-Fi 注意事項
↓
可變電阻
↓
LDR 光敏電阻
↓
電壓換算
↓
ADC + PWM
↓
光線自動調光

一、ADC 是什麼?

ADC:

Analog-to-Digital Converter

功能就是把:

Analog Voltage

轉換成:

Digital Number

例如:

輸入電壓
     │
     ▼
   ESP32 ADC
     │
     ▼
數位數值

假設使用 12-bit ADC,

程式可能看到:

0
512
1024
2048
3072
4095

這些數字就代表不同的類比輸入程度。

Espressif 官方說明,ADC Conversion 的本質就是把輸入類比電壓轉換成數位 Raw Data,供微控制器進一步處理。

二、ESP32 ADC 是幾位元?

Arduino-ESP32 目前 analogRead() 的預設解析度為:

12-bit

也就是:

2^12
=
4096

個數位階級。

因此一般看到的 Raw ADC 範圍就是:

0 ~ 4095

官方 Arduino-ESP32 文件也明確指出:

analogReadResolution(12);

所對應的範圍是:

0 ~ 4095

而且目前所有支援的 Arduino-ESP32 SoC 預設 analogRead() 回傳解析度都是 12-bit;原始 ESP32 的硬體解析度可設定在 9~12 bit。

三、為什麼 12-bit 是 0~4095?

因為:

2^12 = 4096

總共有:

4096 個狀態

但是數字從:

0

開始。

所以最大值是:

4095

也就是:

0
1
2
3
...
4093
4094
4095

共:

4096

個數值。

因此:

12-bit ADC
=
0 ~ 4095

四、ESP32 ADC 與 Arduino UNO 有什麼差別?

傳統 Arduino UNO 常見 ADC 是:

10-bit

所以:

0 ~ 1023

ESP32 Arduino 預設則為:

12-bit

因此:

0 ~ 4095

簡單比較:

Arduino UNO 與 ESP32 ADC 解析度比較
平台 ADC Resolution Raw Range
Arduino UNO 10-bit 0~1023
ESP32 Arduino 預設 12-bit 0~4095

因此從 Arduino UNO 轉到 ESP32 時,不要直接假設:

analogRead() 最大值 = 1023

在本章 12-bit 設定下應該使用:

4095

Arduino-ESP32 官方目前的 ADC API 預設值確實為 12-bit、0~4095。

五、ESP32 analogRead()

Arduino 環境中最簡單的 ADC 函式就是:

analogRead();

格式:

uint16_t value =
  analogRead(pin);

例如:

int adcValue =
  analogRead(34);

代表:

讀取 GPIO34 的 ADC Raw Value。

Arduino-ESP32 官方定義:

uint16_t analogRead(uint8_t pin);

回傳的是:

未校準的 ADC Raw Value。

六、第一個 ESP32 ADC 程式

本章以常見原始 ESP32 開發板為例,先使用:

GPIO34

作為類比輸入。

const uint8_t ADC_PIN = 34;

void setup()
{
  Serial.begin(115200);

  analogReadResolution(12);
}

void loop()
{
  int adcValue =
    analogRead(ADC_PIN);

  Serial.print("ADC = ");
  Serial.println(adcValue);

  delay(500);
}

Serial Monitor 可能看到:

ADC = 432
ADC = 780
ADC = 1540
ADC = 2217
ADC = 3250

數字會依輸入電壓而改變。

七、analogReadResolution()

Arduino-ESP32 可以使用:

analogReadResolution(bits);

設定 analogRead() 回傳值的解析度。

本章使用:

analogReadResolution(12);

因此範圍:

0 ~ 4095

官方文件指出,目前預設值就是 12-bit;若設定不同的解析度,Arduino-ESP32 會依 SoC 能力改變硬體解析度或對讀值進行位元調整。

所以即使預設已經是:

12-bit

教材中仍可以明確寫:

analogReadResolution(12);

讓讀者清楚知道目前使用的 ADC Range。

八、ESP32 ADC1 與 ADC2 是什麼?

原始 ESP32 具有兩組 ADC:

ADC1
ADC2

Espressif 官方文件指出,原始 ESP32 有兩個 SAR ADC,最多可以提供 18 個類比量測 Channel。

對常見原始 ESP32 而言,ADC Pin 大致可分為:

ADC1

GPIO32
GPIO33
GPIO34
GPIO35
GPIO36
GPIO37
GPIO38
GPIO39

也就是:

GPIO32 ~ GPIO39

共 8 個 Channel。

ADC2

原始 ESP32 的 ADC2 包括:

GPIO0
GPIO2
GPIO4
GPIO12
GPIO13
GPIO14
GPIO15
GPIO25
GPIO26
GPIO27

共 10 個 Channel。

但是:

不代表每一塊 ESP32 開發板都把這些 GPIO 全部拉到排針,也不代表所有腳位都適合在你的電路中自由使用。

有些 GPIO 同時具有:

Boot Strapping
Flash
Debug
板上電路
其他特殊功能

因此仍應以實際開發板 Pinout 為準。Espressif 官方也提醒,不同 SoC、Module 與 Board 的 ADC 數量、Channel 與可用 Pin 並不相同。

九、ADC1 / ADC2 最重要的差異:Wi-Fi

這是 ESP32 ADC 教學中非常重要的一點:

ADC2 與 Wi-Fi 會有資源衝突

Espressif 官方目前仍明確指出:

ADC2 同時會被 Wi-Fi 使用。

官方 GPIO 文件更直接建議:

如果使用 Wi-Fi 時遇到 ADC2 GPIO 無法正常讀取,可改用 ADC1 GPIO。

因此對初學者而言,可以記住:

ESP32 專案要使用 Wi-Fi
+
需要 ADC
↓
優先考慮 ADC1

例如本章選:

GPIO34

就是 ADC1。

十、為什麼 ESP32 IoT 專案推薦 ADC1?

假設後面做:

ESP32
+
LDR
+
Wi-Fi
+
MQTT
+
Web Server

如果一開始把 LDR 接在 ADC2,

可能遇到:

Wi-Fi 開啟
↓
ADC2 資源被共用
↓
ADC 讀取產生問題

官方 ESP32 GPIO 文件仍明確列出:

ADC2 pins cannot be used when Wi-Fi is used

並建議改用 ADC1 GPIO。

因此本系列 ESP32 教材在有 Wi-Fi 擴充可能性的 ADC 專案中,優先使用:

GPIO32 ~ GPIO39

中的合適 ADC1 Pin。

十一、注意:不同 ESP32 型號不能直接套用同一張 ADC Pin 表

ESP32 現在是一整個系列:

ESP32
ESP32-S2
ESP32-S3
ESP32-C3
ESP32-C6
...

它們:

ADC Channel
GPIO Mapping
Voltage Range
ADC 數量

並不完全相同。

Arduino-ESP32 官方 ADC 文件也特別提醒:

每一種 SoC / Module 的 ADC 數量、Channels 與可用 Pins 可能不同。

因此本文的:

ADC1 GPIO32~39
ADC2 GPIO0、2、4、12~15、25~27

主要指:

原始 ESP32

例如常見的 ESP32-WROOM-32 類開發板。

不要直接把這張表套到 ESP32-C3 或 ESP32-S3。

十二、本章為什麼選 GPIO34?

本章 ADC Input 使用:

const uint8_t ADC_PIN = 34;

主要是因為對常見原始 ESP32:

GPIO34

屬於:

ADC1

因此適合後續加入:

Wi-Fi
MQTT
Web Server
IoT

功能。原始 ESP32 的 ADC1 Channel 位於 GPIO32~39。

另外要注意,GPIO34~39 在原始 ESP32 上屬於輸入用途為主的 GPIO,因此很適合作為 ADC Sensor Input,但不要把它們當一般輸出腳使用。實際 Board 的可用 Pin 仍應查看對應硬體資料。

十三、實驗一:ESP32 + 可變電阻

可變電阻是學習 ADC 最容易理解的元件之一。

常見:

10K Potentiometer

具有三個腳位:

A
Wiper
B

接線:

3.3V
 │
 │
┌─────────┐
│   POT   │
└─────────┘
 │   │
 │   └──────── GPIO34
 │
GND

整理成:

ESP32 可變電阻 ADC 接線表
可變電阻 ESP32
外側腳 1 3.3V
中間腳 GPIO34
外側腳 2 GND

轉動旋鈕時:

中間腳電壓

會隨位置變化。

ESP32 ADC 就可以讀出不同數值。

十四、可變電阻完整程式

const uint8_t POT_PIN = 34;

void setup()
{
  Serial.begin(115200);

  analogReadResolution(12);
}

void loop()
{
  int adcValue =
    analogRead(POT_PIN);

  Serial.print(
    "Potentiometer ADC = "
  );

  Serial.println(
    adcValue
  );

  delay(200);
}

轉動旋鈕時,Serial Monitor 可以觀察:

ADC = 238
ADC = 726
ADC = 1420
ADC = 2186
ADC = 3165
ADC = 3890

理想化概念上:

低電壓
↓
ADC 小

高電壓
↓
ADC 大

十五、ESP32 ADC 可以直接用 3.3 / 4095 算電壓嗎?

教學中常見這個公式:

Voltage =
ADC Value × 3.3 / 4095

例如:

float voltage =
  adcValue * 3.3 / 4095.0;

這可以幫助理解:

ADC Raw
→
Voltage

的數學關係。

但是:

它不應被當成 ESP32 精密電壓計算法

原因是 ESP32 ADC 的:

實際 Vref
Attenuation
非線性
晶片個體差異

都會影響結果。

Espressif 官方指出,原始 ESP32 ADC 的設計參考電壓約為 1100 mV,但不同晶片實際 Vref 可能約落在 1000~1200 mV,因此官方提供 ADC Calibration 機制來提高電壓換算準確度。

所以:

adcValue * 3.3 / 4095.0

比較適合:

理解 ADC 比例概念。

如果要取得比較可靠的實際電壓:

優先考慮 analogReadMilliVolts() 或 ESP-IDF ADC Calibration。

十六、analogReadMilliVolts()

Arduino-ESP32 官方提供:

analogReadMilliVolts(pin);

例如:

uint32_t voltageMv =
  analogReadMilliVolts(ADC_PIN);

官方文件指出:

analogRead()

回傳的是:

Raw ADC

而:

analogReadMilliVolts()

會回傳經校準換算後的:

mV

數值。

因此:

1650 mV

就是:

1.650 V

十七、ESP32 ADC 電壓量測範例

const uint8_t ADC_PIN = 34;

void setup()
{
  Serial.begin(115200);

  analogReadResolution(12);
}

void loop()
{
  uint16_t raw =
    analogRead(ADC_PIN);

  uint32_t millivolts =
    analogReadMilliVolts(
      ADC_PIN
    );

  float voltage =
    millivolts / 1000.0;

  Serial.print(
    "RAW = "
  );

  Serial.print(
    raw
  );

  Serial.print(
    "  Voltage = "
  );

  Serial.print(
    voltage,
    3
  );

  Serial.println(
    " V"
  );

  delay(500);
}

Serial Monitor 可能看到:

RAW = 1258  Voltage = 1.012 V
RAW = 1875  Voltage = 1.493 V
RAW = 2490  Voltage = 1.985 V

實際數字會依:

ESP32 晶片
ADC Attenuation
輸入電壓
校準資料
硬體雜訊

而有所不同。analogReadMilliVolts() 是 Arduino-ESP32 官方提供的校準毫伏轉換 API。

十八、什麼是 ADC Attenuation?

ESP32 ADC 內部參考電壓並不是簡單等於:

3.3V

要量測較高輸入電壓時,ADC 可以透過:

Attenuation

先對輸入訊號進行衰減。

Arduino-ESP32 目前提供:

analogSetAttenuation();

以及:

analogSetPinAttenuation();

可設定 ADC Attenuation。

十九、ESP32 ADC Attenuation 設定

以 Arduino-ESP32 為例,可使用:

analogSetPinAttenuation(
  ADC_PIN,
  ADC_11db
);

官方目前列出的 Arduino API 選項包括:

ADC_0db
ADC_2_5db
ADC_6db
ADC_11db

對原始 ESP32,官方 Arduino 文件列出的典型可量測範圍大約是:

ESP32 ADC Attenuation 量測範圍
Attenuation 原始 ESP32 官方文件量測範圍
ADC_0db 約 0.10~0.95 V
ADC_2_5db 約 0.10~1.25 V
ADC_6db 約 0.15~1.75 V
ADC_11db 約 0.15~3.10 V

不同 ESP32 SoC 的範圍並不相同。

所以:

不要把「ESP32 是 3.3V 系統」直接理解成「ADC 一定可以精確量到 0~3.3V」。

二十、本章 ADC 初始化建議

因此本章正式範例可以寫:

const uint8_t ADC_PIN = 34;

void setup()
{
  Serial.begin(115200);

  analogReadResolution(12);

  analogSetPinAttenuation(
    ADC_PIN,
    ADC_11db
  );
}

這樣程式就明確指定:

Resolution = 12-bit
Attenuation = ADC_11db

而不是依賴不明確的預設狀態。

二十一、ESP32 ADC 可以直接量 5V 嗎?

不可以把 5V 直接送進 ESP32 ADC GPIO。

ESP32 是低電壓 MCU,ADC Attenuation 的作用是擴展 ADC 的內部可量測範圍:

它不是把 GPIO 變成 5V tolerant。

如果要量:

5V
9V
12V
電池組
更高電壓

需要先利用:

Voltage Divider

把電壓降低到 ESP32 GPIO 安全範圍。

二十二、什麼是電阻分壓?

最基本的 Voltage Divider:

Vin
 │
R1
 │
 ├──────── ADC
 │
R2
 │
GND

輸出電壓:

Vout =
Vin × R2 / (R1 + R2)

例如:

R1 = 10K
R2 = 10K

則:

Vout
=
Vin × 1/2

所以:

Vin = 5V

理論上:

Vout = 2.5V

ESP32 ADC 量到的是:

Vout

程式再反推:

Vin

二十三、分壓電路電壓反推

因為:

Vout =
Vin × R2 / (R1 + R2)

所以:

Vin =
Vout × (R1 + R2) / R2

如果:

R1 = 10K
R2 = 10K

就是:

Vin =
Vout × 2

程式:

float inputVoltage =
  adcVoltage
  * (R1 + R2)
  / R2;

但是實際設計還需要考慮:

ADC 最大安全輸入
Attenuation Range
電阻誤差
ADC 校準
ADC Input Impedance
雜訊
保護電路

因此精密電壓量測不能只靠一條公式。

二十四、LDR 光敏電阻是什麼?

LDR:

Light Dependent Resistor

中文通常稱為:

光敏電阻

它的電阻值會隨環境光線改變。

一般概念:

光線變強
↓
LDR 電阻下降
光線變暗
↓
LDR 電阻升高

但是 ESP32 ADC 量的是:

Voltage

不是:

Resistance

所以需要利用:

Voltage Divider

把 LDR 的電阻變化轉成電壓變化。

二十五、ESP32 LDR 分壓電路

本章使用:

3.3V
 │
LDR
 │
 ├──────── GPIO34
 │
10KΩ
 │
GND

接線:

ESP32 LDR 分壓電路接線表
元件 接線
LDR 一端 ESP32 3.3V
LDR 另一端 GPIO34
10KΩ 一端 GPIO34
10KΩ 另一端 GND

GPIO34 連接:

LDR
+
10K

中間的分壓點。

二十六、這個 LDR 電路的亮暗關係

在本章接法:

3.3V
↓
LDR
↓
ADC
↓
10K
↓
GND

光線越強:

LDR Resistance ↓

分壓點電壓通常:

↑

因此 ADC Raw:

↑

也就是:

亮
↓
ADC 大

相反:

暗
↓
LDR Resistance ↑
↓
ADC Voltage ↓
↓
ADC 小

所以程式可以利用:

ADC 小
=
環境暗

來提高 LED 亮度。

二十七、ESP32 LDR 基本程式

const uint8_t LDR_PIN = 34;

void setup()
{
  Serial.begin(115200);

  analogReadResolution(12);

  analogSetPinAttenuation(
    LDR_PIN,
    ADC_11db
  );
}

void loop()
{
  int lightValue =
    analogRead(LDR_PIN);

  Serial.print(
    "LDR ADC = "
  );

  Serial.println(
    lightValue
  );

  delay(200);
}

用手遮住 LDR,

觀察:

ADC

如何改變。

再用手機手電筒照射 LDR,

再觀察:

ADC

數值。

這是實際建立:

光線
↔
ADC

關係最好的方式。

二十八、不要直接假設「2000 就一定代表某個 Lux」

這一點很重要。

LDR 的:

Resistance

與:

Light Intensity

並不是簡單的固定線性比例。

而且:

LDR 型號
固定電阻值
供電電壓
ADC 特性
安裝位置
入射角
環境光源

都會影響結果。

因此本章:

ADC = 2000

只能理解成:

此硬體配置下的一個相對亮度數值。

如果要得到:

Lux

需要知道 LDR 的特性曲線並進行校準,或者直接使用數位光照度 Sensor。

二十九、ESP32 ADC 為什麼會跳動?

實際測量時可能看到:

1820
1827
1816
1832
1824

即使光線看起來沒有變化。

這是正常需要處理的工程問題。

ESP32 ADC 會受到:

電源雜訊
RF / Wi-Fi
Sensor Noise
走線
ADC 本身非線性
參考電壓
外部干擾

等因素影響。

Espressif 官方 ADC Calibration 文件也專門提供「降低雜訊」相關建議,因為 ADC 實際量測並不是完全沒有 Noise。

三十、最簡單的 ADC 平均濾波

可以連續取:

10 次

再平均。

int readAverageADC(
  uint8_t pin,
  int samples
)
{
  long total = 0;

  for (
    int i = 0;
    i < samples;
    i++
  )
  {
    total += analogRead(pin);

    delay(2);
  }

  return total / samples;
}

使用:

int value =
  readAverageADC(
    LDR_PIN,
    10
  );

這樣數值通常會比單次:

analogRead()

穩定。

三十一、ESP32 ADC 平均讀取完整範例

const uint8_t LDR_PIN = 34;

const int SAMPLE_COUNT = 10;

int readAverageADC(
  uint8_t pin
)
{
  uint32_t total = 0;

  for (
    int i = 0;
    i < SAMPLE_COUNT;
    i++
  )
  {
    total +=
      analogRead(pin);

    delay(2);
  }

  return
    total / SAMPLE_COUNT;
}

void setup()
{
  Serial.begin(115200);

  analogReadResolution(12);

  analogSetPinAttenuation(
    LDR_PIN,
    ADC_11db
  );
}

void loop()
{
  int adcValue =
    readAverageADC(
      LDR_PIN
    );

  Serial.print(
    "Average ADC = "
  );

  Serial.println(
    adcValue
  );

  delay(200);
}

三十二、ADC 數值如何轉成百分比?

12-bit ADC:

0 ~ 4095

如果只是要顯示相對百分比,可以使用:

int percent =
  map(
    adcValue,
    0,
    4095,
    0,
    100
  );

例如:

ADC = 0
→ 0%

ADC ≈ 2048
→ 50%

ADC = 4095
→ 100%

但要注意:

這只是 ADC Raw 的比例,不代表 LDR 的 Lux 百分比。

三十三、ESP32 ADC + PWM 是非常重要的組合

前一章已經學過:

PWM

可以控制 LED:

0 ~ 255

本章 ADC 則是:

0 ~ 4095

所以可以建立:

ADC Sensor
↓
0~4095
↓
map()
↓
0~255
↓
PWM
↓
LED

這就是一個完整的:

Sensor → Controller → Output

控制系統。

三十四、實作:ESP32 光線感測器 + LED 自動亮度控制

這就是本章主要實作。

目標:

環境很亮
↓
LED 變暗

環境變暗
↓
LED 變亮

形成:

自動感光燈

使用:

LDR
+
ADC
+
PWM
+
LED

三十五、實作材料

ESP32 ADC 光線感測實作材料清單
材料 數量
ESP32 開發板 1
LDR 光敏電阻 1
10KΩ 電阻 1
LED 1
220Ω~330Ω 電阻 1
麵包板 1
杜邦線 若干

GPIO:

LDR ADC → GPIO34
LED PWM → GPIO18

三十六、完整接線

LDR

3.3V
 │
 │
LDR
 │
 ├──────── GPIO34
 │
10KΩ
 │
 │
GND

LED

GPIO18
 │
220Ω
 │
LED +
 │
LED -
 │
GND

整理:

ESP32 ADC 光線感測器與 LED 接線表
ESP32 元件
3.3V LDR 一端
GPIO34 LDR / 10KΩ 分壓中心點
GND 10KΩ 另一端
GPIO18 220Ω~330Ω → LED 正極
GND LED 負極

三十七、ADC 值如何控制 PWM?

我們現在有:

ADC
0 ~ 4095

LED PWM:

0 ~ 255

但是因為我們希望:

環境亮
→ LED 暗

因此必須反向 Mapping。

int ledDuty =
  map(
    lightValue,
    0,
    4095,
    255,
    0
  );

也就是:

ADC 小
↓
PWM 大
↓
LED 亮

以及:

ADC 大
↓
PWM 小
↓
LED 暗

三十八、ESP32 光線自動調光第一版

const uint8_t LDR_PIN = 34;
const uint8_t LED_PIN = 18;

const uint32_t PWM_FREQ = 5000;
const uint8_t PWM_RESOLUTION = 8;

void setup()
{
  Serial.begin(115200);

  analogReadResolution(12);

  analogSetPinAttenuation(
    LDR_PIN,
    ADC_11db
  );

  ledcAttach(
    LED_PIN,
    PWM_FREQ,
    PWM_RESOLUTION
  );
}

void loop()
{
  int lightValue =
    analogRead(
      LDR_PIN
    );

  int ledDuty =
    map(
      lightValue,
      0,
      4095,
      255,
      0
    );

  ledDuty =
    constrain(
      ledDuty,
      0,
      255
    );

  ledcWrite(
    LED_PIN,
    ledDuty
  );

  Serial.print(
    "Light = "
  );

  Serial.print(
    lightValue
  );

  Serial.print(
    "  LED Duty = "
  );

  Serial.println(
    ledDuty
  );

  delay(100);
}

三十九、為什麼實際效果可能不理想?

如果直接把:

0~4095

映射到:

255~0

可能發現:

大部分環境

只落在例如:

800~2800

而不是完整:

0~4095

結果 LED 只會在中間一小段變化。

所以實際做 Sensor 時,最好先進行:

Calibration

四十、先找出實際亮暗 ADC 值

先只執行:

Serial.println(
  analogRead(LDR_PIN)
);

然後測試:

最暗

例如得到:

600

最亮

例如得到:

3200

那就不要使用:

map(
  adc,
  0,
  4095,
  255,
  0
);

而改成:

map(
  adc,
  600,
  3200,
  255,
  0
);

這樣實際效果會更明顯。

四十一、LDR 校準參數

例如設定:

const int DARK_ADC = 600;
const int BRIGHT_ADC = 3200;

然後:

int ledDuty =
  map(
    lightValue,
    DARK_ADC,
    BRIGHT_ADC,
    255,
    0
  );

再:

ledDuty =
  constrain(
    ledDuty,
    0,
    255
  );

這比直接假設 ADC 一定會跑滿:

0~4095

更符合實際 Sensor 應用。

四十二、正式版:ESP32 光線感測器 + LED 自動亮度控制

下面這一支程式適合作為本章正式下載範例。

/*
  第 6 章
  ESP32 ADC 類比輸入完整教學

  實作:
  ESP32 光線感測器
  + LED 自動亮度控制

  功能:
  1. GPIO34 讀取 LDR
  2. 12-bit ADC
  3. ADC_11db
  4. 10 次平均濾波
  5. ADC 校準範圍
  6. ADC 轉換為 LED PWM
  7. 環境越暗 LED 越亮
  8. Serial Monitor 顯示 ADC / mV / PWM

  適用:
  常見原始 ESP32 / ESP32-WROOM-32 類開發板

  LDR:
  3.3V
    |
   LDR
    |
    +------ GPIO34
    |
   10K
    |
   GND

  LED:
  GPIO18
    |
   220R
    |
   LED
    |
   GND
*/


// -----------------------------
// GPIO
// -----------------------------
const uint8_t LDR_PIN = 34;

const uint8_t LED_PIN = 18;


// -----------------------------
// ADC
// -----------------------------
const uint8_t ADC_BITS = 12;

const int ADC_MAX = 4095;


// -----------------------------
// LDR Calibration
// 請依實際環境重新校準
// -----------------------------
const int DARK_ADC = 600;

const int BRIGHT_ADC = 3200;


// -----------------------------
// ADC Average
// -----------------------------
const int SAMPLE_COUNT = 10;


// -----------------------------
// PWM
// -----------------------------
const uint32_t PWM_FREQ = 5000;

const uint8_t PWM_RESOLUTION = 8;

const int PWM_MAX = 255;


// -----------------------------
// Read Average ADC
// -----------------------------
int readAverageADC(
  uint8_t pin
)
{
  uint32_t total = 0;

  for (
    int i = 0;
    i < SAMPLE_COUNT;
    i++
  )
  {
    total +=
      analogRead(pin);

    delay(2);
  }

  return
    total / SAMPLE_COUNT;
}


// -----------------------------
// setup()
// -----------------------------
void setup()
{
  Serial.begin(115200);

  delay(500);

  Serial.println();
  Serial.println(
    "ESP32 ADC + LDR + PWM Demo"
  );


  // -------------------------
  // ADC
  // -------------------------
  analogReadResolution(
    ADC_BITS
  );

  analogSetPinAttenuation(
    LDR_PIN,
    ADC_11db
  );


  // -------------------------
  // LED PWM
  // Arduino-ESP32 3.x
  // -------------------------
  if (!ledcAttach(
        LED_PIN,
        PWM_FREQ,
        PWM_RESOLUTION))
  {
    Serial.println(
      "LEDC initialization failed!"
    );

    while (true)
    {
      delay(1000);
    }
  }


  ledcWrite(
    LED_PIN,
    0
  );


  Serial.println(
    "System Ready"
  );
}


// -----------------------------
// loop()
// -----------------------------
void loop()
{
  // -------------------------
  // Read LDR
  // -------------------------
  int lightValue =
    readAverageADC(
      LDR_PIN
    );


  // -------------------------
  // Read calibrated mV
  // -------------------------
  uint32_t voltageMv =
    analogReadMilliVolts(
      LDR_PIN
    );


  // -------------------------
  // Dark -> Bright LED
  // Bright -> Dim LED
  // -------------------------
  int ledDuty =
    map(
      lightValue,
      DARK_ADC,
      BRIGHT_ADC,
      PWM_MAX,
      0
    );


  ledDuty =
    constrain(
      ledDuty,
      0,
      PWM_MAX
    );


  // -------------------------
  // PWM Output
  // -------------------------
  ledcWrite(
    LED_PIN,
    ledDuty
  );


  // -------------------------
  // Relative Light %
  // -------------------------
  int lightPercent =
    map(
      lightValue,
      DARK_ADC,
      BRIGHT_ADC,
      0,
      100
    );


  lightPercent =
    constrain(
      lightPercent,
      0,
      100
    );


  // -------------------------
  // Serial Monitor
  // -------------------------
  Serial.print(
    "ADC = "
  );

  Serial.print(
    lightValue
  );


  Serial.print(
    " / "
  );

  Serial.print(
    ADC_MAX
  );


  Serial.print(
    " | Voltage = "
  );

  Serial.print(
    voltageMv
  );

  Serial.print(
    " mV"
  );


  Serial.print(
    " | Light = "
  );

  Serial.print(
    lightPercent
  );

  Serial.print(
    "%"
  );


  Serial.print(
    " | LED PWM = "
  );

  Serial.println(
    ledDuty
  );


  delay(100);
}

本程式的 ADC 部分使用 Arduino-ESP32 官方目前提供的 analogReadResolution()、analogSetPinAttenuation()、analogRead() 與 analogReadMilliVolts() API。

四十三、完整程式執行流程

ESP32 開機:

setup()
↓
Serial 115200
↓
ADC = 12-bit
↓
GPIO34 = ADC Input
↓
ADC_11db
↓
GPIO18 = LEDC PWM
↓
System Ready

接著:

loop()
↓
LDR ADC 讀取 10 次
↓
平均
↓
取得 Light Value
↓
轉換成 LED PWM
↓
輸出 GPIO18
↓
顯示 Serial Data
↓
重新循環

四十四、環境變暗時發生什麼?

環境變暗:

Light ↓

LDR Resistance:

↑

在本章分壓方式下:

ADC Voltage ↓

所以:

ADC Value ↓

程式:

map(
  lightValue,
  DARK_ADC,
  BRIGHT_ADC,
  255,
  0
);

因此:

ADC ↓
↓
PWM Duty ↑
↓
LED 變亮

形成:

自動夜燈

四十五、環境變亮時發生什麼?

光線增加:

Light ↑

LDR Resistance:

↓

ADC Voltage:

↑

ADC:

↑

因此:

PWM Duty ↓

LED:

變暗

也就是:

白天
↓
LED 暗

晚上
↓
LED 亮

四十六、為什麼要使用平均值?

如果每次直接:

analogRead()

可能看到:

1801
1827
1796
1814
1835

PWM 也跟著:

124
122
126
124
121

LED 就可能出現輕微抖動。

使用:

10 Samples Average

後,例如變成:

1814
1817
1812
1815

控制會比較穩定。

這是 Sensor Control 很基本也很實用的:

Filtering

概念。

四十七、進階:再加入 Deadband

如果希望 LED 更穩,

還可以設定:

Deadband

例如只有 ADC 變化超過:

20

才更新 PWM。

if (
  abs(
    lightValue -
    lastLightValue
  ) >= 20
)
{
  lastLightValue =
    lightValue;

  // 更新 LED
}

這樣可以避免 ADC 在:

1800
1805
1797
1808

附近微小跳動時一直更新 LED。

四十八、進階:用指數平滑濾波

除了平均,

還可以使用:

Exponential Moving Average

例如:

filteredValue =
  filteredValue * 0.9
  +
  newValue * 0.1;

效果是:

Sensor 突然改變
↓
輸出不會立刻跳動
↓
而是平滑跟隨

這很適合:

光線
溫度
電池電壓
旋鈕
Joystick

等 Sensor。

四十九、ADC 電壓為什麼不一定完全線性?

ESP32 ADC 很方便,

但不能把它當成:

精密桌上型數位電表。

Espressif 官方指出,ESP32 不同晶片的 ADC 參考電壓存在個體差異,設計值約 1100 mV,但實際可能約 1000~1200 mV,因此需要 Calibration 來降低 Vref 差異造成的誤差。

所以:

ADC Raw

適合:

相對量測
Sensor Threshold
控制
亮度
旋鈕
趨勢

如果要做:

高精度電壓計
精密資料擷取
精密電池監控
高解析 Sensor

則應考慮:

ADC Calibration
External ADC
精密 Reference
前端濾波
精密電阻

等設計。

五十、什麼時候適合使用 analogReadMilliVolts()?

如果只是:

LDR 亮暗

其實使用:

analogRead()

即可。

因為只需要比較:

大
小

但如果想顯示:

Sensor Voltage

則:

analogReadMilliVolts()

更方便。

例如:

uint32_t mv =
  analogReadMilliVolts(
    ADC_PIN
  );

官方 Arduino-ESP32 API 會將 ADC 結果轉換為校準後的 mV 數值。

五十一、Raw ADC 與 Voltage 最好同時保留

工程上很建議同時記錄:

RAW
+
mV

例如:

RAW = 1832
Voltage = 1486 mV

原因是:

RAW

方便:

Threshold
map()
Filtering
Debug

而:

mV

方便:

理解實際訊號
電壓紀錄
校準
硬體檢查

五十二、ADC1 + Wi-Fi 的實際設計策略

假設之後專案變成:

ESP32
+
LDR
+
Wi-Fi
+
MQTT
+
Web Dashboard

建議架構:

LDR
↓
ADC1 GPIO
↓
analogRead()
↓
Filtering
↓
Light Value
↓
PWM LED
+
Wi-Fi Upload

不要到了最後才發現:

Sensor 接到 ADC2
+
Wi-Fi 開啟

造成 ADC 資源使用問題。

對原始 ESP32,官方仍建議 Wi-Fi 與 ADC 同時使用時優先選 ADC1。

五十三、ESP32 Continuous ADC

本章主要使用:

analogRead()

也就是:

OneShot

每呼叫一次:

analogRead()

就做一次轉換。

但目前 Arduino-ESP32 還提供:

ADC Continuous Mode

可以在背景持續進行多個 ADC Conversion,並取得平均 Raw 與 mV 結果。官方 Arduino-ESP32 文件目前也提供 analogContinuous()、analogContinuousStart()、analogContinuousRead() 等 API。

對本章:

LDR
Potentiometer
一般 Sensor

使用:

analogRead()

就已經足夠。

Continuous ADC 可以留到高速資料擷取或進階 Sensor Sampling 再學。

五十四、常見錯誤:把 ADC 接到 5V

這是最需要避免的硬體錯誤之一。

不要:

5V
↓
ESP32 ADC GPIO

如果要量高於 ADC / GPIO 允許範圍的電壓,

必須先:

Voltage Divider
↓
Protection
↓
ESP32 ADC

而且要確認:

最大輸入

即使異常狀況也不會超過 ESP32 Pin 的電氣限制。

五十五、常見錯誤:ADC 一定等於 3.3 / 4095

這只是簡化的理論比例。

ESP32 實際 ADC 會受到:

Vref
Attenuation
Calibration
Non-linearity
Chip Variation

影響。

因此:

raw * 3.3 / 4095.0

適合教學理解,

而需要實際電壓時,應優先使用:

analogReadMilliVolts()

或更完整的 ADC Calibration。官方也明確指出實際 Vref 會有晶片個體差異。

五十六、常見錯誤:ADC2 + Wi-Fi

例如:

GPIO25

在原始 ESP32 屬於 ADC2。

如果專案同時:

Wi-Fi ON

則 ADC2 會涉及 Wi-Fi 資源共享問題。

所以:

Wi-Fi + Analog Sensor

入門專案優先使用:

ADC1

例如:

GPIO32
GPIO33
GPIO34
GPIO35
GPIO36
GPIO39

再依開發板實際 Pinout 挑選。Espressif 官方 GPIO 文件仍明確建議 Wi-Fi 使用時遇到 ADC2 問題可改用 ADC1。

五十七、常見錯誤:把 ESP32-S3 Pin 表當 ESP32 Pin 表

不要看到:

ESP32

三個字就認為所有版本都完全一樣。

例如:

ESP32
ESP32-S2
ESP32-S3
ESP32-C3
ESP32-C6

ADC GPIO Mapping 並不完全相同。

Arduino-ESP32 官方因此沒有在通用 API 文件中宣稱所有 SoC 共用相同 ADC Pin 表,而是要求依各 SoC / Module Datasheet 判斷。

五十八、常見錯誤:LDR 接法不同,亮暗結果相反

如果:

3.3V
↓
LDR
↓
ADC
↓
10K
↓
GND

一般會得到:

越亮 → ADC 越大

如果改成:

3.3V
↓
10K
↓
ADC
↓
LDR
↓
GND

則結果會反過來:

越亮 → ADC 越小

所以不要只複製:

map()

一定要先確認自己的:

Voltage Divider Direction

五十九、常見錯誤:LED 自動調光一直閃

可能原因:

ADC Noise
LDR Noise
電源雜訊
光線本身變動
PWM Mapping 太敏感

可以依序加入:

Average
↓
Calibration
↓
Deadband
↓
Smoothing

改善。

這就是從「範例程式」進入「真正控制系統」的重要一步。

六十、ESP32 ADC 可以應用在哪些專題?

掌握 ADC 後,可以做:

可變電阻控制器

Potentiometer
↓
ADC
↓
PWM
↓
LED / Motor

自動感光燈(補充說明)

LDR
↓
ADC
↓
PWM LED

Joystick

VRx
VRy
↓
2 路 ADC

電池電壓監測

Battery
↓
Voltage Divider
↓
ADC

類比溫度感測器

Sensor Voltage
↓
ADC
↓
Temperature

土壤濕度 Sensor

Analog Output
↓
ADC
↓
Moisture Level

類比氣體 Sensor

Sensor
↓
ADC
↓
Threshold
↓
Alarm

六十一、ADC 與 PWM 的關係非常重要

目前已經學過:

第 5 章
PWM

以及:

第 6 章
ADC

現在已經形成完整控制鏈:

Real World
↓
Sensor
↓
ADC
↓
ESP32
↓
程式演算法
↓
PWM
↓
Actuator

例如本章:

環境光
↓
LDR
↓
ADC
↓
ESP32
↓
map()
↓
PWM
↓
LED

這就是很多自動控制系統最基本的架構。

六十二、FAQ:ESP32 ADC 常見問題

Q1:ESP32 ADC 是幾 bit?

Arduino-ESP32 目前 analogRead() 預設為:

12-bit

因此一般 Raw Range:

0 ~ 4095

Q2:ESP32 analogRead() 回傳什麼?

回傳指定 GPIO / ADC Channel 的:

Raw ADC Value

官方 API:

uint16_t analogRead(
  uint8_t pin
);

Q3:ESP32 ADC 0~4095 代表 0~3.3V 嗎?

不能這麼簡化理解。

ADC Raw 與實際電壓關係會受到:

Vref
Attenuation
Calibration
ADC Characteristics

影響。

官方指出原始 ESP32 的設計 Vref 約 1100 mV,但不同晶片實際可能約為 1000~1200 mV。

Q4:如何取得 ESP32 ADC 電壓?

Arduino-ESP32 可以使用:

analogReadMilliVolts(pin);

取得經校準轉換後的:

mV

值。

Q5:ESP32 ADC1 與 ADC2 有什麼差異?

對原始 ESP32:

ADC1
GPIO32~39

ADC2:

GPIO0
GPIO2
GPIO4
GPIO12~15
GPIO25~27

ADC2 還與 Wi-Fi 共用相關硬體資源,因此 Wi-Fi ADC 專案通常優先選 ADC1。

Q6:ESP32 使用 Wi-Fi 時 ADC2 可以用嗎?

這是需要特別注意的限制。

官方目前仍明確說明:

ADC2 is also used by Wi-Fi

並在 ESP32 GPIO 文件建議 Wi-Fi 使用時改用 ADC1 GPIO 避免 ADC2 問題。

Q7:ESP32 ADC 可以直接量 5V 嗎?

不應直接將 5V 接到 ESP32 ADC GPIO。

應使用:

Voltage Divider

把電壓降低至安全範圍後再量測。

Q8:LDR 為什麼需要電阻?

因為:

LDR

本身主要改變的是:

Resistance

ESP32 ADC 量的是:

Voltage

所以使用固定電阻與 LDR 組成:

Voltage Divider

把 Resistance Change 轉換成 Voltage Change。

Q9:ESP32 ADC 為什麼一直跳?

ADC 真實量測會受到:

Noise
Power
Sensor
RF
ADC Characteristics

等影響。

可以使用:

Average
Moving Average
Low-pass Filter
Deadband
Calibration

改善。ESP-IDF 官方 ADC Calibration 文件本身也提供降低 ADC Noise 的相關章節。

Q10:ADC 值可以直接控制 PWM 嗎?

可以。

例如:

ADC
0~4095

透過:

map()

轉成:

PWM
0~255

再使用:

ledcWrite();

即可控制 LED Brightness。

六十三、本章學習重點整理

第一個核心:

ADC(補充說明)

是:

Analog Voltage
↓
Digital Value

第二個核心:

Arduino-ESP32 預設:

12-bit

所以:

0 ~ 4095

第三個核心:

最基本讀取:

analogRead(
  ADC_PIN
);

如果需要電壓:

analogReadMilliVolts(
  ADC_PIN
);

第四個核心:

對原始 ESP32:

ADC1
GPIO32~39
ADC2
GPIO0、2、4、12~15、25~27

第五個核心:

Wi-Fi 專案優先考慮 ADC1

因為:

ADC2
+
Wi-Fi

有硬體資源共享限制。

第六個核心:

LDR 必須使用:

Voltage Divider

才能把:

Resistance

轉成:

Voltage

供 ADC 讀取。

第七個核心:

ADC 不應直接被當成:

精密電壓計

實際量測需要注意:

Vref
Attenuation
Calibration
Noise
Non-linearity

Espressif 也提供專門的 ADC Calibration Driver 處理這些誤差來源。

六十四、本章總結

在第 6 章中,我們正式學會 ESP32:

ADC 類比輸入

首先理解:

ADC
=
Analog
↓
Digital

接著掌握:

12-bit
↓
0~4095

並使用:

analogRead();

取得 ADC Raw Value。

需要電壓時,可以使用:

analogReadMilliVolts();

取得 Arduino-ESP32 提供的校準毫伏值。

我們也認識原始 ESP32 的:

ADC1
ADC2

其中 ADC2 與 Wi-Fi 有硬體資源共享限制,因此後續 IoT Sensor 專案應優先考慮 ADC1。

接著實作:

Potentiometer
↓
ADC

以及:

LDR
↓
Voltage Divider
↓
ADC

最後把前一章 PWM 與本章 ADC 串接:

光線
↓
LDR
↓
ESP32 ADC
↓
0~4095
↓
Filtering
↓
Calibration
↓
map()
↓
0~255
↓
LEDC PWM
↓
LED Brightness

完成:

ESP32 光線感測器 + LED 自動亮度控制

這一章真正重要的地方,是開始建立:

Sensor Input → Data Processing → Output Control

的完整控制系統觀念。

掌握這個架構後,下一步就能很自然地延伸到:

Joystick 控制
電池電壓監測
類比溫度 Sensor
土壤濕度
氣體 Sensor
光線監控
ADC + OLED
ADC + Wi-Fi
ADC + MQTT
智慧照明
IoT Sensor Node

等更完整的 ESP32 專案。

下一步學習

不要停在這一篇,接著把內容做成自己的實作

目前主題:ESP32 / IoT。從完整學習路線、免費資源、會員下載,再接到實作所需材料。

同系列:ESP32 |  已是會員?直接前往 Getfile
相關資源與會員文章下載
文章可免費閱讀;下載完整 DOCX 或 PDF 需登入免費基本會員。
查看相關資源 會員免費下載 DOCX 會員免費 PDF/列印

← 回文章列表