0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 程式開發
  3. 正文

ESP32 電容式觸摸感測器完整教學:touchRead、觸摸中斷、Deep Sleep 喚醒與滑桿實作

2026 年 6 月 18 日 633點熱度 0人點贊 0條評論

ESP32 電容式觸摸感測器完整教學封面

ESP32 電容式觸摸感測器

如果你的專案需要一顆按鈕,但你不想開孔、不想擔心進水、也不想面對機械接點的磨損 —— 那 ESP32 其實已經把答案內建在晶片裡了。

ESP32 內建 10 通道電容式觸摸感測器(Capacitive Touch Sensor),透過測量 RC 震盪電路的放電次數來偵測觸摸。這項功能在 IoT 裝置中非常實用:觸控按鈕、滑桿、近距離感測、水位偵測,甚至連 Deep Sleep 的觸摸喚醒都靠它。

與傳統機械按鈕比較

先講結論 —— 它不是「比較炫的按鈕」,而是在幾個面向上真的勝出:

特性 電容式觸摸 機械按鈕
使用壽命 無限(無實體磨損) 約 5~50 萬次
防水防塵 可穿透塑膠/玻璃外殼 需開孔,易進水
電路設計 僅需一根訊號線 需上拉電阻 + 去彈跳電容
成本 內建,零成本 元件成本 0.1~1 元
功耗 最低約 10 uA(觸摸喚醒) 需輪詢,功耗較高

其中最容易被低估的是「可穿透外殼」這一點:你的產品可以是一整片平整的塑膠或玻璃,沒有開孔、沒有縫,也沒有會卡灰塵的接點。

工作原理

它的原理其實很樸素,就是量「充電要多久」:

  1. 觸摸腳位內部整合一個恆流源,對寄生電容(Cp)充電
  2. 當電壓達到閾值時,觸發放電(透過內部開關快速放電)
  3. 充放電次數由 RTC 計數器(RTC_CNT)計數
  4. 同樣時間內,觸摸(手指增加寄生電容 Cfinger)會使充電速度變慢,計數值降低
  5. 比對計數值與閾值,判斷是否觸摸

RC 充放電與脈衝計數

關鍵關係式:

充電時間 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 觸摸感測器腳位對應

注意這是 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。這代表你可以做一個「放著半年、按一下才醒」的裝置。

觸摸喚醒 Deep Sleep 時序

// 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() 把每個通道的原始值印出來看一整天,確認漂移幅度有多大,再決定閾值與適應速度。跳過這一步直接寫死閾值,大概兩天後就會回來重寫。

標籤: 教學
最後更新:2026 年 9 月 30 日

shi6a

這個人很懶,什麼都沒留下

點贊
< 上一篇
下一篇 >

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang