回到首頁

手機直連 ESP32-CAM 控制兩顆 Servo 與 ESC 油門

讓 ESP32-CAM 自己開 Wi-Fi 熱點,手機連上後用瀏覽器控制方向舵、升降舵與無刷馬達 ESC 油門。

一、系統概念

這個設計讓手機不需要經過路由器,直接連上 ESP32-CAM 的 Wi-Fi 熱點,然後在手機網頁上用三個滑桿控制 RC 飛機的基本動作。

手機瀏覽器
滑桿控制
ESP32-CAM
Wi-Fi AP
GPIO PWM
控制訊號
Servo × 2
ESC × 1
方向舵 Servo

控制 rudder,讓飛機左右偏航。

升降舵 Servo

控制 elevator,讓飛機抬頭或低頭。

ESC 油門

用 1000–2000 μs PWM 脈衝控制無刷馬達轉速。

安全提醒:第一次測試 ESC 與無刷馬達時,務必先拆掉螺旋槳。

二、硬體接線建議

以下以常見的 AI Thinker ESP32-CAM 為例。測試階段建議先不要使用 SD 卡,避免 GPIO 腳位衝突。

ESP32-CAM 腳位分配

功能 ESP32-CAM 腳位 說明
方向舵 Servo GPIO13 輸出 50 Hz PWM 訊號
升降舵 Servo GPIO14 輸出 50 Hz PWM 訊號
ESC 油門訊號 GPIO15 輸出 1000–2000 μs 油門 PWM
共地 GND ESP32-CAM、Servo、ESC 必須共地
5V 供電 5V 建議由 UBEC 或 ESC BEC 供電

Servo 三條線接法

Servo 線色 接法
咖啡 / 黑 GND
5V
橘 / 黃 / 白 GPIO13 或 GPIO14

ESC 訊號線接法

ESC 訊號線 接法
黑 / 咖啡 GND
5V BEC,可視情況使用
白 / 黃 / 橘 GPIO15
建議供電架構:2S LiPo → ESC → 無刷馬達;2S LiPo → UBEC 5V 2A–3A → ESP32-CAM + 兩顆 Servo。
如果 ESC 本身有 BEC,而且輸出能力足夠,也可以使用 ESC 的 5V 供電。但若已經使用外接 UBEC,就不要同時把 ESC 的紅色 5V 線接上,以避免兩個 5V 電源互相衝突。

三、手機操作方式

ESP32-CAM 開機後會建立 Wi-Fi 熱點。手機連上這個熱點後,用瀏覽器進入控制頁面。

Wi-Fi SSID ESP32CAM_RC
Wi-Fi 密碼 12345678
控制網頁 http://192.168.4.1

控制頁面會有三個滑桿:

Rudder

方向舵角度,範圍 0–180°,中立值 90°。

Elevator

升降舵角度,範圍 0–180°,中立值 90°。

Throttle

ESC 油門脈衝,範圍 1000–2000 μs,最低值 1000 μs。

四、Arduino IDE 設定

請先安裝 ESP32 開發板套件,並安裝伺服馬達函式庫。

項目 設定
開發板 AI Thinker ESP32-CAM
函式庫 ESP32Servo
上傳模式 GPIO0 接 GND 後上電進入燒錄模式;上傳完成後移除 GPIO0–GND,重新開機。

五、完整 ESP32-CAM 控制程式

這版先控制兩顆 Servo 與 ESC,不開啟攝影串流。建議先用這版確認舵面與油門穩定,再整合 ESP32-CAM 影像。

#include <WiFi.h>
#include <WebServer.h>
#include <ESP32Servo.h>

WebServer server(80);

// ===== Wi-Fi AP 設定 =====
const char* ssid = "ESP32CAM_RC";
const char* password = "12345678";

// ===== 腳位設定 =====
const int RUDDER_PIN   = 13;  // 方向舵
const int ELEVATOR_PIN = 14;  // 升降舵
const int ESC_PIN      = 15;  // ESC 油門

Servo rudderServo;
Servo elevatorServo;
Servo esc;

// ===== 初始值 =====
int rudderAngle = 90;
int elevatorAngle = 90;
int throttleUs = 1000;   // ESC 最低油門

unsigned long lastCommandTime = 0;
const unsigned long FAILSAFE_TIME = 2000; // 2 秒沒收到手機指令就關油門

// ===== 網頁內容 =====
String htmlPage() {
  String page = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ESP32-CAM RC Control</title>
<style>
  body {
    font-family: Arial, sans-serif;
    text-align: center;
    background: #f5f5f5;
    margin: 0;
    padding: 20px;
  }
  h1 {
    font-size: 28px;
  }
  .box {
    background: white;
    padding: 20px;
    margin: 18px auto;
    max-width: 420px;
    border-radius: 12px;
    box-shadow: 0 0 8px rgba(0,0,0,0.15);
  }
  input[type=range] {
    width: 90%;
  }
  .value {
    font-size: 22px;
    font-weight: bold;
  }
  .warning {
    color: red;
    font-weight: bold;
  }
</style>
</head>
<body>

<h1>ESP32-CAM RC Control</h1>

<p class="warning">測試時請先拆掉螺旋槳</p>

<div class="box">
  <h2>方向舵 Rudder</h2>
  <input id="rudder" type="range" min="0" max="180" value="90">
  <p>角度:<span id="rudderVal" class="value">90</span>°</p>
</div>

<div class="box">
  <h2>升降舵 Elevator</h2>
  <input id="elevator" type="range" min="0" max="180" value="90">
  <p>角度:<span id="elevatorVal" class="value">90</span>°</p>
</div>

<div class="box">
  <h2>ESC 油門</h2>
  <input id="throttle" type="range" min="1000" max="2000" value="1000">
  <p>脈衝寬度:<span id="throttleVal" class="value">1000</span> μs</p>
  <button onclick="stopMotor()" style="font-size:22px;padding:10px 30px;">立即關油門</button>
</div>

<script>
const rudder = document.getElementById("rudder");
const elevator = document.getElementById("elevator");
const throttle = document.getElementById("throttle");

const rudderVal = document.getElementById("rudderVal");
const elevatorVal = document.getElementById("elevatorVal");
const throttleVal = document.getElementById("throttleVal");

function updateDisplay() {
  rudderVal.innerHTML = rudder.value;
  elevatorVal.innerHTML = elevator.value;
  throttleVal.innerHTML = throttle.value;
}

function sendAll() {
  updateDisplay();

  fetch("/set?r=" + rudder.value +
        "&e=" + elevator.value +
        "&t=" + throttle.value)
    .catch(err => console.log(err));
}

function stopMotor() {
  throttle.value = 1000;
  sendAll();
}

rudder.oninput = sendAll;
elevator.oninput = sendAll;
throttle.oninput = sendAll;

// 每 300 ms 自動送一次,作為連線心跳
setInterval(sendAll, 300);

updateDisplay();
</script>

</body>
</html>
)rawliteral";

  return page;
}

// ===== 首頁 =====
void handleRoot() {
  server.send(200, "text/html", htmlPage());
}

// ===== 接收手機控制指令 =====
void handleSet() {
  if (server.hasArg("r")) {
    rudderAngle = constrain(server.arg("r").toInt(), 0, 180);
  }

  if (server.hasArg("e")) {
    elevatorAngle = constrain(server.arg("e").toInt(), 0, 180);
  }

  if (server.hasArg("t")) {
    throttleUs = constrain(server.arg("t").toInt(), 1000, 2000);
  }

  rudderServo.write(rudderAngle);
  elevatorServo.write(elevatorAngle);
  esc.writeMicroseconds(throttleUs);

  lastCommandTime = millis();

  server.send(200, "text/plain", "OK");
}

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

  // ===== Servo / ESC 初始化 =====
  rudderServo.setPeriodHertz(50);
  elevatorServo.setPeriodHertz(50);
  esc.setPeriodHertz(50);

  rudderServo.attach(RUDDER_PIN, 500, 2400);
  elevatorServo.attach(ELEVATOR_PIN, 500, 2400);

  // ESC 通常使用 1000–2000 μs
  esc.attach(ESC_PIN, 1000, 2000);

  // 初始安全狀態
  rudderServo.write(90);
  elevatorServo.write(90);
  esc.writeMicroseconds(1000);

  delay(3000); // 給 ESC 啟動 / 解鎖時間

  // ===== 啟動 Wi-Fi AP =====
  WiFi.softAP(ssid, password);

  Serial.println();
  Serial.println("ESP32-CAM RC Controller Started");
  Serial.print("AP IP address: ");
  Serial.println(WiFi.softAPIP());

  // ===== 網頁伺服器 =====
  server.on("/", handleRoot);
  server.on("/set", handleSet);

  server.begin();

  lastCommandTime = millis();
}

void loop() {
  server.handleClient();

  // ===== 失控保護 =====
  // 如果手機超過 2 秒沒有送指令,油門降到最低,舵面回中
  if (millis() - lastCommandTime > FAILSAFE_TIME) {
    throttleUs = 1000;
    rudderAngle = 90;
    elevatorAngle = 90;

    esc.writeMicroseconds(throttleUs);
    rudderServo.write(rudderAngle);
    elevatorServo.write(elevatorAngle);

    lastCommandTime = millis();
  }
}

六、ESC 校正與解鎖注意

大部分飛機用 ESC 的油門 PWM 範圍如下:

PWM 脈衝寬度 意義
1000 μs 最低油門 / 停止
1500 μs 中間值
2000 μs 最高油門
ESC 校正與測試時一定要拆掉螺旋槳。無刷馬達瞬間啟動時非常危險。

一般測試流程

  1. 拆掉螺旋槳。
  2. 開啟 ESP32-CAM。
  3. 確認手機網頁油門滑桿在 1000 μs
  4. 接上 2S LiPo 給 ESC。
  5. 聽到 ESC 啟動嗶聲後,慢慢推油門。

常見 ESC 油門校正流程

不同 ESC 的流程可能不同,但常見流程如下:

  1. 先送出 2000 μs 最高油門訊號。
  2. 接上 ESC 電源。
  3. 等待 ESC 嗶聲提示。
  4. 改送 1000 μs 最低油門。
  5. 等待確認嗶聲,完成校正。

七、建議測試流程

第一階段

只測兩顆 Servo,不接 ESC。確認方向舵與升降舵會隨滑桿動作。

第二階段

接上 ESC,但拆掉螺旋槳。確認油門從 1000 μs 往上推時馬達會逐漸啟動。

第三階段

裝上螺旋槳前,把飛機固定好,再進行低油門推力測試。

第四階段

確認控制穩定後,再考慮整合 ESP32-CAM 影像串流。

伺服方向相反時

如果方向舵或升降舵動作方向相反,可以在程式中把角度改成 180 - angle

// 方向舵反向範例
rudderServo.write(180 - rudderAngle);

// 升降舵反向範例
elevatorServo.write(180 - elevatorAngle);
若後續要加入即時影像串流,建議先確認控制訊號不會抖動。最穩定的架構通常是 ESP32-CAM 負責影像,另一片 ESP32 / ESP32-S3 負責 Servo 與 ESC 控制。