ESP32 深度睡眠與喚醒模式開發全攻略(定時器、觸控及外部喚醒)

在設計物聯網(IoT)或電池供電的嵌入式專案時,功耗控制是決定專案成敗的關鍵因素。如果讓 ESP32 一直處於正常運作狀態(約 100mA 至 240mA 的電流消耗),電池很快就會耗盡。

幸運的是,ESP32 提供了強大的深度睡眠(Deep Sleep)模式。在深度睡眠中,主要 CPU、大部分的 RAM 以及所有數位週邊都會被關閉,僅保留 RTC(即時時鐘)控制器、RTC 週邊與 RTC 記憶體運作,此時功耗可以降至 10µA 至 150µA 之間。

本教學將帶您完整掌握如何在 Arduino IDE 中設定 ESP32 進入深度睡眠,並詳細解說四種不同的喚醒機制:定時器喚醒觸控喚醒外部單腳喚醒 (ext0)外部多腳喚醒 (ext1),並附上接線原理圖與完整程式碼。


1. ESP32 功耗模式與為什麼選擇深度睡眠?

ESP32 支援 5 種不同的電源模式,我們可以根據應用需求靈活切換:

電源模式CPUWi-Fi / 藍牙RTC 週邊核心功耗(典型值)
Active (運作)運作中運作中運作中95mA ~ 240mA
Modem-sleep運作中關閉運作中15mA ~ 20mA
Light-sleep暫停 (泡泡)關閉運作中~0.8mA
Deep-sleep關閉關閉運作中10µA ~ 150µA
Hibernation (冬眠)關閉關閉關閉~5µA
ESP32 官方數據手冊電源模式功耗對比表
ESP32 官方各睡眠模式功耗參數
ESP32 正常運作模式下功耗數據
電池供電功耗流失對比
ESP32 DOIT DEVKIT V1 完整腳位圖

深度睡眠的喚醒本質:軟重啟

需要特別注意的是,當 ESP32 從深度睡眠中喚醒時,它不會從它睡著的那一行代碼繼續執行,而是會重新執行 setup()。這相當於一次軟重啟(Soft Reset)。

如何在重啟後保留數據?(RTC 記憶體應用)

因為重啟後一般 RAM 中的變數都會遺失,如果我們想在睡眠重啟後保留某些數據(例如:記錄累計開機次數 bootCount),我們必須使用關鍵字 RTC_DATA_ATTR 將變數宣告儲存在 RTC 記憶體(RTC Fast Memory)中:

RTC_DATA_ATTR int bootCount = 0; // 此變數將儲存在 RTC 記憶體中,睡眠重啟後不會遺失

2. 喚醒源實作一:定時器喚醒(Timer Wake Up)

定時器喚醒是最簡單、最常用的喚醒方式。讓 ESP32 沉睡指定的秒數後,自動醒來執行任務(例如讀取感測器、上傳資料到雲端),隨後再次入睡。

定時器喚醒工作流示意圖
定時喚醒串口日誌畫面

接線示意圖

此模式純屬內部時鐘計時,不需要連接任何外部硬體腳位。

示範程式碼

/*
 * ESP32 定時器喚醒範例
 */
#define uS_TO_S_FACTOR 1000000ULL  // 微秒轉換為秒的係數
#define TIME_TO_SLEEP  5           // 睡眠時間(秒)

RTC_DATA_ATTR int bootCount = 0;   // 記錄開機次數

void print_wakeup_reason(){
  esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();
  switch(wakeup_reason) {
    case ESP_SLEEP_WAKEUP_TIMER:     Serial.println("喚醒原因:定時器時間到"); break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD:  Serial.println("喚醒原因:觸控喚醒"); break;
    case ESP_SLEEP_WAKEUP_EXT0:      Serial.println("喚醒原因:外部信號喚醒 (ext0)"); break;
    case ESP_SLEEP_WAKEUP_EXT1:      Serial.println("喚醒原因:外部信號喚醒 (ext1)"); break;
    default:                         Serial.printf("喚醒原因:非深度睡眠喚醒 (代碼: %d)\n", wakeup_reason); break;
  }
}

void setup(){
  Serial.begin(115200);
  delay(1000); // 讓串口監視器有時間初始化

  bootCount++;
  Serial.println("開機/喚醒次數: " + String(bootCount));
  print_wakeup_reason();

  // 設定定時喚醒源
  esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
  Serial.println("設定 ESP32 進入深度睡眠,將在 " + String(TIME_TO_SLEEP) + " 秒後醒來...");
  
  Serial.println("正在入睡...");
  Serial.flush();
  esp_deep_sleep_start();
  Serial.println("這行代碼永遠不會被執行");
}

void loop(){
  // 深度睡眠下 loop 內不會運行
}

3. 喚醒源實作二:觸控喚醒(Touch Wake Up)

ESP32 內建電容式觸摸感應器,擁有多個觸摸引腳(Touch Pins)。我們可以利用這些引腳,讓使用者透過「摸一下導線」來喚醒睡眠中的 ESP32。

ESP32 觸控引腳對照

ESP32 共有 10 個觸控腳位,在 Arduino IDE 中分別以 T0T9 標註:

  • T0 (GPIO 4)
  • T1 (GPIO 0)
  • T2 (GPIO 2)
  • T3 (GPIO 15)
  • T4 (GPIO 13)
  • T5 (GPIO 12)
  • T6 (GPIO 14)
  • T7 (GPIO 27)
  • T8 (GPIO 33)
  • T9 (GPIO 32)

接線示意圖

在觸控腳位(例如 T3, 即 GPIO 15)連接一根杜邦線,並在其末端接上一片銅片(或直接觸摸杜邦線的金屬裸露端即可)。

觸控腳位內部電路關係
手動觸控測試實照
觸控喚醒串口日誌畫面

示範程式碼

/*
 * ESP32 觸控喚醒範例
 */
#define Threshold 40 // 觸摸閾值。數值越大越靈敏,若常誤觸請調低(通常在 20-50 之間)

RTC_DATA_ATTR int bootCount = 0;

void print_wakeup_reason(){
  esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();
  if(wakeup_reason == ESP_SLEEP_WAKEUP_TOUCHPAD){
    Serial.println("喚醒原因:觸控引腳觸發");
  }
}

void print_wakeup_touchpad(){
  touch_pad_t touchPin = esp_sleep_get_touchpad_wakeup_status();
  Serial.printf("觸發喚醒的觸控通道為 T%d (GPIO 腳位對應請對照表)\n", touchPin);
}

void callback(){
  // 必須宣告此回呼函數,但由於深度睡眠後會直接走 setup 重啟,因此內容可留空
}

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

  bootCount++;
  Serial.println("開機/喚醒次數: " + String(bootCount));
  print_wakeup_reason();
  if(esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_TOUCHPAD){
    print_wakeup_touchpad();
  }

  // 設定觸控中斷腳位:我們使用 T3 (GPIO 15)
  touchAttachInterrupt(T3, callback, Threshold);

  // 啟用觸控喚醒源
  esp_sleep_enable_touchpad_wakeup();

  Serial.println("設定觸控喚醒完畢,正在入睡...");
  Serial.flush();
  esp_deep_sleep_start();
}

void loop(){}

4. 喚醒源實作三:外部單腳喚醒 (ext0)

外部喚醒利用 GPIO 的電位變化(高電位或低電位)來觸起喚醒。 這需要用到 ESP32 內部擁有 RTC 週邊功能 的腳位(RTC_GPIO)。當 ESP32 處於深度睡眠時,只有以下這些 RTC_GPIO 引腳可以正常工作:

💡 可用於外部喚醒的 RTC_GPIO 腳位:0, 2, 4, 12, 13, 14, 15, 25, 26, 27, 32, 33, 34, 35, 36, 37, 38, 39。

ext0 模式特點

ext0 是針對單一腳位的外部喚醒。當指定的 RTC_GPIO 達到指定電平(1 高電位 或 0 低電位)時,就會喚醒。此模式會保持 RTC 週邊在睡眠時開啟,功耗稍高於 ext1。

接線示意圖

我們將一個按鍵開關連接到 GPIO 33(屬於 RTC_IO)。按鍵一端接 VCC,另一端接 GPIO 33,並加上一個 10KΩ 的下拉電阻(Pull-down)接到 GND,確保按鍵未按下時處於穩定的低電位。

外部單按鍵喚醒 ext0 接線圖
外部單按鍵測試實照
ext0 喚醒串口日誌畫面

示範程式碼

/*
 * ESP32 外部單腳喚醒 (ext0) 範例
 */
RTC_DATA_ATTR int bootCount = 0;

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

  bootCount++;
  Serial.println("開機/喚醒次數: " + String(bootCount));
  
  if(esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_EXT0){
    Serial.println("喚醒原因:由 RTC_IO 外部訊號觸發 (ext0)");
  }

  // 設定 ext0 喚醒:當 GPIO 33 偵測到 高電位 (1) 時喚醒
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_33, 1); 

  Serial.println("設定 ext0 喚醒,正在入睡...");
  Serial.flush();
  esp_deep_sleep_start();
}

void loop(){}

5. 喚醒源實作四:外部多腳喚醒 (ext1)

如果您想使用多個不同的按鍵都可以喚醒 ESP32,您就必須使用 ext1 模式。 ext1 模式使用 RTC 控制器(RTC Controller),因此在睡眠期間不需要開啟 RTC 週邊,功耗最低

ext1 觸發模式

  • ESP_EXT1_WAKEUP_ANY_HIGH:當遮罩中任意一個指定的 GPIO 變為高電位時喚醒。
  • ESP_EXT1_WAKEUP_ALL_LOW:當遮罩中所有指定的 GPIO 同時變為低電位時喚醒。

GPIO 位元遮罩(Bitmask)計算

ext1 需要輸入一個 64 位元的十六進位數值來代表哪些 GPIO 腳位被選用。 計算公式為: $$\text{Bitmask} = 2^{\text{GPIO\_NUM}}$$

例如,若我們想使用 GPIO 2GPIO 15 作為喚醒源:

  • GPIO 2 對應為:$2^2 = 4$
  • GPIO 15 對應為:$2^{15} = 32768$
  • 合併後的位元值為:$4 + 32768 = 32772$
  • 將 32772 轉換為十六進位即為:0x8004

在 Arduino 代碼中,可以直接寫成:

#define BUTTON_PIN_BITMASK 0x8004 // 二進位即代表 GPIO 2 與 GPIO 15

接線示意圖

將兩個按鍵分別接到 GPIO 2GPIO 15。按鍵未按下時均透過 10KΩ 下拉電阻接到 GND;按下時接通 VCC(高電平)。

二進位與十六進位遮罩換算表
外部多按鍵喚醒 ext1 接線圖
外部多按鍵測試實照
ext1 喚醒串口日誌畫面

示範程式碼

/*
 * ESP32 外部多腳喚醒 (ext1) 範例
 */
#define BUTTON_PIN_BITMASK 0x8004 // GPIO 2 和 GPIO 15 的位元遮罩

RTC_DATA_ATTR int bootCount = 0;

void print_GPIO_wake_up(){
  // 獲取觸發喚醒的 GPIO 狀態(回傳的是 64bit 的 Bitmask)
  uint64_t GPIO_reason = esp_sleep_get_ext1_wakeup_status();
  
  // 計算出到底是哪一個 GPIO 觸發了喚醒
  int gpio_num = log(GPIO_reason) / log(2);
  Serial.printf("觸發喚醒的實體腳位為:GPIO %d\n", gpio_num);
}

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

  bootCount++;
  Serial.println("開機/喚醒次數: " + String(bootCount));
  
  if(esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_EXT1){
    Serial.println("喚醒原因:外部多腳訊號觸發 (ext1)");
    print_GPIO_wake_up();
  }

  // 設定 ext1 喚醒:當遮罩中的任意腳位 (GPIO 2 或 15) 變為高電位時喚醒
  esp_sleep_enable_ext1_wakeup(BUTTON_PIN_BITMASK, ESP_EXT1_WAKEUP_ANY_HIGH);

  Serial.println("設定 ext1 喚醒,正在入睡...");
  Serial.flush();
  esp_deep_sleep_start();
}

void loop(){}

💡 總結

ESP32 的深度睡眠模式是物聯網開發者的強大工具。透過不同的喚醒源:

  1. 定時器喚醒:適合定時上傳數據的遙測系統。
  2. 觸控喚醒:適合智慧開關、手動觸發的精緻小家電。
  3. 外部喚醒 (ext0 & ext1):適合警報感應器、開門開關,或低功耗遙控器按鍵。

善用 RTC_DATA_ATTR 來在睡眠期間暫存資料,並根據週邊按鍵數靈活選擇 ext0(單按鍵)或 ext1(多按鍵,功耗更低),能讓您的 ESP32 電池供電專案壽命從原本的數天延長至數月甚至數年!

如果您對 ESP32 在物聯網通訊(如 MQTT 控制)有興趣,歡迎參考我的另一篇文章:物聯網接案實戰:打造無線 Modbus 控制盒與 APP 遠端控制電熱水器系統

如果您在實作睡眠電路時遇到任何問題,歡迎在下方留言討論!

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

返回頂端