一、系統概念
這個設計讓手機不需要經過路由器,直接連上 ESP32-CAM 的 Wi-Fi 熱點,然後在手機網頁上用三個滑桿控制 RC 飛機的基本動作。
手機瀏覽器
滑桿控制
滑桿控制
→
ESP32-CAM
Wi-Fi AP
Wi-Fi AP
→
GPIO PWM
控制訊號
控制訊號
→
Servo × 2
ESC × 1
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 校正與測試時一定要拆掉螺旋槳。無刷馬達瞬間啟動時非常危險。
一般測試流程
- 拆掉螺旋槳。
- 開啟 ESP32-CAM。
- 確認手機網頁油門滑桿在 1000 μs。
- 接上 2S LiPo 給 ESC。
- 聽到 ESC 啟動嗶聲後,慢慢推油門。
常見 ESC 油門校正流程
不同 ESC 的流程可能不同,但常見流程如下:
- 先送出 2000 μs 最高油門訊號。
- 接上 ESC 電源。
- 等待 ESC 嗶聲提示。
- 改送 1000 μs 最低油門。
- 等待確認嗶聲,完成校正。
七、建議測試流程
第一階段
只測兩顆 Servo,不接 ESC。確認方向舵與升降舵會隨滑桿動作。
第二階段
接上 ESC,但拆掉螺旋槳。確認油門從 1000 μs 往上推時馬達會逐漸啟動。
第三階段
裝上螺旋槳前,把飛機固定好,再進行低油門推力測試。
第四階段
確認控制穩定後,再考慮整合 ESP32-CAM 影像串流。
伺服方向相反時
如果方向舵或升降舵動作方向相反,可以在程式中把角度改成 180 - angle。
// 方向舵反向範例
rudderServo.write(180 - rudderAngle);
// 升降舵反向範例
elevatorServo.write(180 - elevatorAngle);
若後續要加入即時影像串流,建議先確認控制訊號不會抖動。最穩定的架構通常是 ESP32-CAM 負責影像,另一片 ESP32 / ESP32-S3 負責 Servo 與 ESC 控制。
本頁為單一 HTML 檔案,可直接上傳至網站或放在教學頁面中使用。