
ESP32 電容式觸摸感測器
如果你的專案需要一顆按鈕,但你不想開孔、不想擔心進水、也不想面對機械接點的磨損 —— 那 ESP32 其實已經把答案內建在晶片裡了。
ESP32 內建 10 通道電容式觸摸感測器(Capacitive Touch Sensor),透過測量 RC 震盪電路的放電次數來偵測觸摸。這項功能在 IoT 裝置中非常實用:觸控按鈕、滑桿、近距離感測、水位偵測,甚至連 Deep Sleep 的觸摸喚醒都靠它。
與傳統機械按鈕比較
先講結論 —— 它不是「比較炫的按鈕」,而是在幾個面向上真的勝出:
| 特性 | 電容式觸摸 | 機械按鈕 |
|---|---|---|
| 使用壽命 | 無限(無實體磨損) | 約 5~50 萬次 |
| 防水防塵 | 可穿透塑膠/玻璃外殼 | 需開孔,易進水 |
| 電路設計 | 僅需一根訊號線 | 需上拉電阻 + 去彈跳電容 |
| 成本 | 內建,零成本 | 元件成本 0.1~1 元 |
| 功耗 | 最低約 10 uA(觸摸喚醒) | 需輪詢,功耗較高 |
其中最容易被低估的是「可穿透外殼」這一點:你的產品可以是一整片平整的塑膠或玻璃,沒有開孔、沒有縫,也沒有會卡灰塵的接點。
工作原理
它的原理其實很樸素,就是量「充電要多久」:
- 觸摸腳位內部整合一個恆流源,對寄生電容(Cp)充電
- 當電壓達到閾值時,觸發放電(透過內部開關快速放電)
- 充放電次數由 RTC 計數器(RTC_CNT)計數
- 同樣時間內,觸摸(手指增加寄生電容 Cfinger)會使充電速度變慢,計數值降低
- 比對計數值與閾值,判斷是否觸摸

關鍵關係式:
充電時間 T_charge = R_eq * C_total
C_total = Cp + Cfinger(手指接近時)
無觸摸 → C_total = Cp → 充電快 → 放電次數多 → 數值高(約 500~800)
有觸摸 → C_total = Cp + Cfinger → 充電慢 → 放電次數少 → 數值低(約 200~400)
看懂這張圖,你就懂了後面所有的調校工作:觸摸不是「多了一個電壓訊號」,而是「充電變慢了」。所以環境的溫度、濕度、外殼厚度、甚至附近有沒有其他通道的銅箔,都會改變基準值。
觸摸感測器 API
ESP32 Arduino 提供了兩組觸摸感測器 API,先決定你要用哪一種再往下寫:
- touchRead(pin):直接讀取觸摸感測器原始計數值(阻塞式)
- touchAttachInterrupt(pin, callback, threshold):設定觸摸中斷(非阻塞)
觸摸腳位對應如下,這張表建議可以直接貼在開發板旁邊:

注意這是 ESP32 經典款的固定對應,不像一般 GPIO 可以隨便挑;如果你用的是 ESP32-S2 或 S3,就自由多了,幾乎每個 GPIO 都能當觸摸通道。
Arduino 程式設計
基礎讀取:touchRead()
最單純的用法就是讀值、比閾值:
// ESP32 觸摸感測器基礎讀取
#define TOUCH_PIN T0 // GPIO 4
void setup() {
Serial.begin(115200);
Serial.println("ESP32 Touch Sensor Demo");
Serial.println("將手指碰觸 GPIO 4 觀察數值變化");
}
void loop() {
int touchVal = touchRead(TOUCH_PIN);
Serial.printf("Touch Value: %d
", touchVal);
if (touchVal < 300) {
Serial.println(">> Touch detected! <<");
} else if (touchVal < 450) {
Serial.println("> Proximity detected <");
}
delay(100);
}
觸摸中斷(非阻塞)
如果需要即時反應,又不希望主迴圈被卡住,就用中斷:
// ESP32 觸摸中斷範例
#define TOUCH_PIN T0 // GPIO 4
#define THRESHOLD 300 // 觸摸閾值(小於此值表示觸摸)
#define LED_PIN 2 // 內建 LED
volatile bool touched = false;
void IRAM_ATTR touchISR() {
touched = true;
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
// 附加中斷:數值低於 threshold 時觸發
touchAttachInterrupt(TOUCH_PIN, touchISR, THRESHOLD);
Serial.println("Touch interrupt enabled, touch GPIO 4...");
}
void loop() {
if (touched) {
touched = false;
Serial.printf("Touch! Value: %d
", touchRead(TOUCH_PIN));
digitalWrite(LED_PIN, !digitalRead(LED_PIN)); // 切換 LED
delay(50); // 簡單去彈跳
}
// 主程式可以做其他工作
delay(10);
}
多重觸摸按鈕
多顆按鈕一起用時,真正的難題是「每個通道的基準值都不一樣」:
// ESP32 多重觸摸按鈕:T0 (GPIO4), T2 (GPIO2), T3 (GPIO15)
#define TOUCH_BTN1 T0
#define TOUCH_BTN2 T2
#define TOUCH_BTN3 T3
#define THRESHOLD 350
struct TouchButton {
uint8_t pin;
uint8_t touchChannel;
bool state; // 目前是否被觸摸
bool lastState;
unsigned long lastDebounce;
};
TouchButton buttons[3] = {
{TOUCH_BTN1, 0, false, false, 0},
{TOUCH_BTN2, 2, false, false, 0},
{TOUCH_BTN3, 3, false, false, 0},
};
void processTouch(TouchButton* btn) {
int val = touchRead(btn->pin);
bool currentlyTouched = (val < THRESHOLD);
// 去彈跳
if (currentlyTouched != btn->lastState) {
btn->lastDebounce = millis();
}
if ((millis() - btn->lastDebounce) > 50) {
if (currentlyTouched != btn->state) {
btn->state = currentlyTouched;
if (btn->state) {
Serial.printf("Button on T%d pressed! (value=%d)
",
btn->touchChannel, val);
}
}
}
btn->lastState = currentlyTouched;
}
void setup() {
Serial.begin(115200);
Serial.println("Multi-touch button demo");
}
void loop() {
for (int i = 0; i < 3; i++) {
processTouch(&buttons[i]);
}
delay(10);
}
觸摸感測器濾波與適應性基準線
這是最關鍵的一段。觸摸感測器會漂移 —— 開機十分鐘後的讀值和剛開機時不同,夏天和冬天也不同。硬寫死的閾值撐不了多久,所以你需要的是一條會自己走的基準線:
// 適應性觸摸閾值(自動校準基準線)
class AdaptiveTouch {
private:
uint8_t _pin;
int _baseline;
int _threshold;
int _smoothVal;
float _alpha; // 濾波係數 0.0~1.0
public:
AdaptiveTouch(uint8_t pin, int thresholdDelta = 100, float alpha = 0.1) {
_pin = pin;
_thresholdDelta = thresholdDelta;
_alpha = alpha;
_baseline = 0;
_smoothVal = 0;
}
void calibrate(int samples = 20) {
long sum = 0;
for (int i = 0; i < samples; i++) {
sum += touchRead(_pin);
delay(10);
}
_baseline = sum / samples;
_smoothVal = _baseline;
_threshold = _baseline - _thresholdDelta;
Serial.printf("Calibrated: baseline=%d, threshold=%d
",
_baseline, _threshold);
}
bool isTouched() {
int raw = touchRead(_pin);
// 指數移動平均濾波
_smoothVal = _alpha * raw + (1 - _alpha) * _smoothVal;
// 如果不處於觸摸狀態,緩慢更新基準線(適應環境變化)
if (_smoothVal > _baseline - _thresholdDelta/2) {
_baseline = _alpha * _smoothVal + (1 - _alpha) * _baseline;
_threshold = _baseline - _thresholdDelta;
}
return (_smoothVal < _threshold);
}
int getSmooth() { return _smoothVal; }
int getBaseline() { return _baseline; }
};

上圖是理想情況下的樣子:手指按下時計數值往下掉、離開後回升,而基準線靜靜地待著不動。真實世界裡基準線會慢慢漂移,所以程式必須定期更新它 —— 但千萬別在觸摸發生的當下更新,否則手指按著的讀值會被當成新的基準,判定就永遠不會觸發了。
觸摸中斷時序與狀態機
去彈跳(debounce)在觸摸按鈕上比機械按鈕更重要,因為電容感測對環境雜訊很敏感。下面這張狀態機把中斷該怎麼處理畫清楚:

重點是那條「抖動」的回退路徑:連續確認次數不足時,要能乾淨地回到 IDLE,而不是卡在等待狀態。少了這條路徑,你的按鈕會偶爾「卡住不回應」。
Deep Sleep 觸摸喚醒
ESP32 最大的優勢之一,是在 Deep Sleep 模式下觸摸感測器仍然可以喚醒主 CPU,功耗僅 10~50 uA。這代表你可以做一個「放著半年、按一下才醒」的裝置。

// ESP32 Deep Sleep 觸摸喚醒範例
// 觸摸 GPIO 4 (T0) 喚醒 ESP32
#define TOUCH_WAKE_PIN T0 // GPIO 4
#define WAKE_THRESHOLD 300
void setup() {
Serial.begin(115200);
delay(100);
// 檢查是什麼原因喚醒
esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
if (cause == ESP_SLEEP_WAKEUP_TOUCH) {
Serial.println("=== 觸摸喚醒!===");
Serial.printf("Touch value: %d
", touchRead(TOUCH_WAKE_PIN));
} else {
Serial.println("=== 正常開機 ===");
// 首次開機,校準觸摸閾值
int baseline = 0;
for (int i = 0; i < 10; i++) {
baseline += touchRead(TOUCH_WAKE_PIN);
delay(10);
}
baseline /= 10;
Serial.printf("Baseline: %d
", baseline);
// 設定觸摸喚醒閾值
touchSleepWakeUpEnable(TOUCH_WAKE_PIN, WAKE_THRESHOLD);
}
// 正常工作 5 秒後進入 Deep Sleep
Serial.println("Active for 5 seconds then sleep...");
delay(5000);
Serial.println("Entering Deep Sleep. Touch GPIO 4 to wake!");
// 進入 Deep Sleep
esp_deep_sleep_start();
}
void loop() {
// 永不執行
}
觸摸喚醒 + 定時喚醒(雙重喚醒源)
實務上很少只靠觸摸喚醒 —— 通常還會加一個定時器,讓裝置能定期自己醒來回報狀態:
// ESP32 雙重喚醒源:觸摸按鈕 或 定時器
#define TOUCH_WAKE_PIN T0
#define WAKE_THRESHOLD 350
#define SLEEP_SECONDS 3600 // 1 小時
void setup() {
Serial.begin(115200);
// 設定觸摸喚醒
touchSleepWakeUpEnable(TOUCH_WAKE_PIN, WAKE_THRESHOLD);
// 設定定時喚醒
esp_sleep_enable_timer_wakeup(SLEEP_SECONDS * 1000000ULL);
// 檢查喚醒源
esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
if (cause == ESP_SLEEP_WAKEUP_TOUCH) {
Serial.println("觸摸按鈕喚醒 — 執行互動工作");
// 例如:發送 HTTP 請求、顯示畫面
} else if (cause == ESP_SLEEP_WAKEUP_TIMER) {
Serial.println("定時喚醒 — 執行定期上報");
// 例如:上傳感測器資料
}
// 工作完成後再次睡眠
Serial.println("Work done, sleeping again...");
esp_deep_sleep_start();
}
觸摸滑桿(Slider)實作
既然可以量出連續的變化量,那就能做滑桿。做法是排一排電極,再比較各通道讀值的變化比例:

// ESP32 觸摸滑桿:使用 3 個觸摸通道實現線性滑桿
#define TCH_A T0 // GPIO 4
#define TCH_B T2 // GPIO 2
#define TCH_C T3 // GPIO 15
// 滑桿位置插值
float calculateSliderPos(int valA, int valB, int valC) {
// 將觸摸值反轉為「接近度」(越小 = 越接近)
float proxA = max(0, 500 - valA);
float proxB = max(0, 500 - valB);
float proxC = max(0, 500 - valC);
float total = proxA + proxB + proxC;
if (total < 10) return -1.0; // 沒有觸摸
// 加權平均計算位置 (0.0 ~ 1.0)
float pos = (proxA * 0.0 + proxB * 0.5 + proxC * 1.0) / total;
return pos;
}
void setup() {
Serial.begin(115200);
}
void loop() {
int va = touchRead(TCH_A);
int vb = touchRead(TCH_B);
int vc = touchRead(TCH_C);
float pos = calculateSliderPos(va, vb, vc);
if (pos >= 0) {
Serial.printf("Slider: %.2f [A=%d B=%d C=%d]
", pos, va, vb, vc);
}
delay(50);
}
近距離感測(Proximity)
同一個原理,把電極做大一點,就變成「不用碰到」的感測 —— 這也是很多檯燈與開關面板的做法:
// ESP32 近距離感測:大面積電極(如銅箔)可感測 5~15 cm 內的人手
#define PROX_PIN T0
void setup() {
Serial.begin(115200);
// Proximity 模式不需要特殊設定,只需降低閾值靈敏度
}
void loop() {
int val = touchRead(PROX_PIN);
Serial.printf("Proximity: %d
", val);
if (val < 350) {
Serial.println(">> Hands detected! <<");
} else if (val < 450) {
Serial.println("-> Approaching...");
}
delay(200);
}
ESP-IDF 直接操作
如果你要更底層的控制(自訂量測時間、閾值與觸發模式),就得直接碰 ESP-IDF 的觸摸 API:
// ESP-IDF 觸摸感測器底層 API
#include "driver/touch_pad.h"
#define TOUCH_PAD T0
void setup() {
// 初始化觸摸 pad
touch_pad_init();
// 設定觸摸通道
touch_pad_config(TOUCH_PAD);
// 設定濾波參數
touch_pad_filter_start(10); // 取樣視窗大小
// 設定中斷閾值
touch_pad_set_thresh(TOUCH_PAD, 300);
touch_pad_isr_register(touchISR, NULL);
touch_pad_intr_enable();
}
void loop() {
uint16_t touchVal;
touch_pad_read_raw_data(TOUCH_PAD, &touchVal);
printf("Touch raw: %u
", touchVal);
uint16_t filteredVal;
touch_pad_read_filtered(TOUCH_PAD, &filteredVal);
printf("Touch filtered: %u
", filteredVal);
}
實務注意事項
這一節幾乎是整篇最值錢的部分,因為觸摸感測的問題九成出在硬體配置而不是程式:
- 參考電容:觸摸腳位與 GND 之間可並聯 10pF 電容,減少外部雜訊干擾
- 絕緣層:可穿透 1~5mm 塑膠或玻璃面板(不需要開孔),建議 PCB 銅箔面積 10x10mm 以上
- 防水:表面使用防水塗層(如三防漆),或使用密封外殼
- 環境漂移:溫度、濕度、灰塵會改變基準線,建議使用適應性濾波器定期校準
- 電源雜訊:Wi-Fi 發射時可能干擾觸摸感測器(高功率射頻耦合),可在觸摸腳位加 RC 濾波
- 多通道串擾:相鄰觸摸通道之間建議保留至少 2mm 間距,減少寄生電容耦合
- ESP32-S2/S3:可使用任何 GPIO 作為觸摸通道(最多 14 通道)
其中「Wi-Fi 發射會干擾觸摸」這條特別容易讓人誤判為程式 bug —— 因為症狀通常是「連上 Wi-Fi 之後按鈕就偶爾失靈」,看起來完全不像硬體問題。
應用案例
最後看別人怎麼用,順便對照自己的需求:
| 應用 | 電極設計 | 觸摸通道數 | 特殊考量 |
|---|---|---|---|
| 觸控按鈕 | 圓形/方形銅箔 10x10mm | 1 | 去彈跳 + 適應性閾值 |
| 線性滑桿 | 3~5 個梯形電極陣列 | 3~5 | 加權插值計算位置 |
| 圓形滾輪 | 4~8 片扇形電極 | 4~8 | 相位差檢測旋轉方向 |
| 近距離感測 | 大面積銅箔 (30x30mm) | 1 | 靈敏度閾值需調低 |
| 水位偵測 | PCB 平行條狀電極 | 1 | 液體導電性差異 |
| 金屬觸控 (隔空) | 金屬螺絲/彈簧 | 1 | 需透過 10k 電阻隔離 ESD |
總結
ESP32 內建的電容式觸摸感測器是 IoT 互動設計的利器:不用加任何 IC,就能做出觸控按鈕、滑桿、近距離感測與超低功耗喚醒。真正決定成敗的不是程式,而是電極設計與適應性濾波這兩件事。

把決策流程攤成文字,選型參考就是這幾句:
- 簡單觸控開關:1 通道 + touchRead() + 閾值判斷(最簡)
- 多按鈕面板:3~5 通道 + touchAttachInterrupt() + 去彈跳
- 類比式滑桿/滾輪:3~8 通道 + 加權插值
- 低功耗喚醒:1 通道 + Deep Sleep + touchSleepWakeUpEnable()
- 隔空感測:大面積電極 + 適應性濾波 + 高靈敏度
我自己的開發順序是:先用 touchRead() 把每個通道的原始值印出來看一整天,確認漂移幅度有多大,再決定閾值與適應速度。跳過這一步直接寫死閾值,大概兩天後就會回來重寫。
文章評論