ESP32 伺服馬達 SG90 控制:PWM 脈衝控制與多軸管理
ESP32 伺服馬達控制是機器人與 IoT 互動裝置中最常見的應用之一。SG90 是一款便宜的微型伺服馬達,廣泛應用於機器手臂、雲台、智慧門鎖、RC 模型等場景。ESP32 伺服馬達控制涉及 PWM 脈衝寬度調變、LEDC 硬體計時器、多通道同步以及電源管理等多個面向。
本文將深入探討:
- 伺服馬達 PWM 控制原理(50 Hz, 1~2 ms 脈衝)
- ESP32 LEDC 硬體 PWM 產生器
- 多軸伺服管理(8+ 通道獨立控制)
- 平滑運動與梯形加減速
- Servo 函式庫 vs 底層 LEDC API
- 電源設計與實務注意事項
SG90 規格
| 參數 | 數值 |
|---|---|
| 工作電壓 | 4.8 ~ 6.0 V |
| 堵轉扭力 (4.8V) | 1.8 kg.cm |
| 轉速 (4.8V) | 0.12 sec/60deg (無負載) |
| 控制訊號 | PWM 50 Hz (20 ms 週期) |
| 脈衝寬度範圍 | 0.5 ~ 2.5 ms |
| 旋轉角度 | 0 ~ 180 deg(部分可 360deg 連續旋轉) |
| 死區 | 約 2~5 us(對應 ~0.2deg) |
| 重量 | 9 g |
| 尺寸 | 23 x 12.2 x 29 mm |
PWM 控制原理
伺服馬達的控制非常簡單:每隔 20 ms(50 Hz)發送一個脈衝,脈衝寬度決定旋轉角度:
角度與脈衝寬度的對應關係:
| 角度 | 脈衝寬度 | Duty Cycle (50 Hz) |
|---|---|---|
| 0° | 1.0 ms | 5.0% |
| 45° | 1.25 ms | 6.25% |
| 90° | 1.5 ms | 7.5% |
| 135° | 1.75 ms | 8.75% |
| 180° | 2.0 ms | 10.0% |

關鍵公式:
脈衝寬度 (ms) = 1.5 + (角度 - 90) / 90
角度範圍: 0 ~ 180 度
脈衝寬度範圍: 0.5 ~ 2.5 ms (SG90 標準)
解析度: 約 11 us/deg (20ms / 180deg / 20bit timer)
硬體接線

⚠ 重要:伺服馬達不可從 ESP32 的 3.3V 取電!
SG90 在堵轉時電流可達 750 mA,遠超過 ESP32 3.3V 穩壓器的輸出能力。必須使用獨立 5V 2A+ 電源,並將 ESP32 GND 與電源 GND 連接(共地)。
ESP32 LEDC PWM 硬體
ESP32 內建 LEDC (LED Control) PWM 控制器,專門用於產生精確的 PWM 訊號:
- 8 通道(可擴展到 16 通道)
- 20-bit 解析度(可設定 1~20 bit)
- 硬體自動輸出(不佔用 CPU)
- 可設定頻率(1 Hz ~ 40 MHz)
- 3 組時基(每組共用頻率,各通道獨立佔空比)
Arduino 程式設計
使用 Servo 函式庫(最簡易)
// ESP32 伺服馬達 — Servo 函式庫
#include <Servo.h>
Servo servo0;
Servo servo1;
void setup() {
Serial.begin(115200);
servo0.attach(13); // GPIO 13
servo1.attach(12); // GPIO 12
// 歸零
servo0.write(0);
servo1.write(0);
delay(1000);
}
void loop() {
// 掃描 0~180 度
for (int pos = 0; pos <= 180; pos += 1) {
servo0.write(pos);
delay(15); // 約需 15 ms/deg (SG90 0.12s/60deg)
}
for (int pos = 180; pos >= 0; pos -= 1) {
servo0.write(pos);
delay(15);
}
// 直接設定脈衝寬度 (us)
servo1.writeMicroseconds(1000); // 0deg
delay(1000);
servo1.writeMicroseconds(1500); // 90deg
delay(1000);
servo1.writeMicroseconds(2000); // 180deg
delay(1000);
}
LEDC 底層 API(精確控制)
// ESP32 伺服馬達 — LEDC 底層 PWM 控制
#define SERVO_FREQ 50 // 50 Hz
#define SERVO_RESOLUTION 16 // 16-bit 解析度
#define SERVO_CH0 0 // LEDC 通道 0
#define SERVO_PIN0 13
// 角度轉脈衝寬度 (us)
int angleToPulse(int angle) {
// angle: 0~180, 回傳 500~2500 us
return map(angle, 0, 180, 500, 2500);
}
// 脈衝寬度轉 duty cycle
uint32_t pulseToDuty(int pulse_us) {
// 50 Hz = 20,000 us 週期
// 16-bit: 0~65535
return (uint64_t)pulse_us * 65535 / 20000;
}
void setServoAngle(int channel, int angle) {
int pulse = angleToPulse(angle);
uint32_t duty = pulseToDuty(pulse);
ledcWrite(channel, duty);
}
void setup() {
// 設定 LEDC 通道
ledcSetup(SERVO_CH0, SERVO_FREQ, SERVO_RESOLUTION);
ledcAttachPin(SERVO_PIN0, SERVO_CH0);
// 設定到 90deg
setServoAngle(SERVO_CH0, 90);
}
void loop() {
for (int a = 0; a <= 180; a += 5) {
setServoAngle(SERVO_CH0, a);
delay(50);
}
}
多軸伺服同步控制
// ESP32 四軸伺服同步控制 (LEDC 多通道)
#define SERVO_FREQ 50
#define SERVO_RES 16
// 伺服設定結構
struct ServoConfig {
uint8_t pin;
uint8_t channel;
int currentAngle;
int targetAngle;
};
ServoConfig servos[4] = {
{13, 0, 90, 0}, // Servo 0: GPIO 13, CH0
{12, 1, 90, 0}, // Servo 1: GPIO 12, CH1
{14, 2, 90, 0}, // Servo 2: GPIO 14, CH2
{27, 3, 90, 0}, // Servo 3: GPIO 27, CH3
};
void setupAllServos() {
for (int i = 0; i < 4; i++) {
ledcSetup(servos[i].channel, SERVO_FREQ, SERVO_RES);
ledcAttachPin(servos[i].pin, servos[i].channel);
setAngle(i, servos[i].currentAngle);
}
}
void setAngle(int idx, int angle) {
servos[idx].targetAngle = constrain(angle, 0, 180);
// 轉換脈衝寬度: 0.5~2.5ms
int pulse = map(angle, 0, 180, 500, 2500);
uint32_t duty = (uint64_t)pulse * 65535 / 20000;
ledcWrite(servos[idx].channel, duty);
}
void moveAllTo(int angles[4]) {
for (int i = 0; i < 4; i++) {
setAngle(i, angles[i]);
}
}
void setup() {
setupAllServos();
}
void loop() {
int pose1[4] = {0, 45, 90, 135};
int pose2[4] = {180, 135, 90, 45};
moveAllTo(pose1);
delay(2000);
moveAllTo(pose2);
delay(2000);
}
平滑運動(梯形加減速)
// 階梯式插值 — 避免伺服馬達抖動
void smoothMove(int idx, int target, int stepDelay = 20) {
int current = servos[idx].currentAngle;
int step = (target > current) ? 1 : -1;
// 逐步移動,每次移動 1 度
for (int pos = current; pos != target; pos += step) {
setAngle(idx, pos);
servos[idx].currentAngle = pos;
delay(stepDelay); // 每度延遲 (ms)
}
// 確保到達目標
setAngle(idx, target);
servos[idx].currentAngle = target;
}
// 自動計算最佳速度
void smoothMoveWithSpeed(int idx, int target, float speedDegPerSec) {
int current = servos[idx].currentAngle;
int dist = abs(target - current);
if (dist == 0) return;
int delayMs = max(5, (int)(1000.0 / speedDegPerSec));
int step = (target > current) ? 1 : -1;
// 梯形加減速
int accelDist = dist / 3; // 加速距離
int decelDist = dist / 3; // 減速距離
for (int pos = current; pos != target; pos += step) {
setAngle(idx, pos);
servos[idx].currentAngle = pos;
int remaining = abs(target - pos);
int traveled = abs(pos - current);
if (traveled < accelDist) {
// 加速: 逐漸減少延遲
delay(delayMs * (1 + (float)(accelDist - traveled) / accelDist));
} else if (remaining < decelDist) {
// 減速: 逐漸增加延遲
delay(delayMs * (1 + (float)(decelDist - remaining) / decelDist));
} else {
// 等速
delay(delayMs);
}
}
}
PCA9685 I2C 擴展(32+ 通道)
// ESP32 + PCA9685 16 通道伺服控制
// PCA9685 使用 I2C 通訊,可控制 16 個伺服馬達
// 多個 PCA9685 可串接 (最多 62 個 = 992 伺服)
#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();
// PCA9685 PWM 頻率設定
#define SERVO_FREQ 50 // 50 Hz
// 脈衝寬度極限 (us)
#define SERVO_MIN 500 // 0deg
#define SERVO_MAX 2500 // 180deg
void setup() {
pwm.begin();
pwm.setOscillatorFrequency(27000000); // PCA9685 內部震盪器
pwm.setPWMFreq(SERVO_FREQ); // 50 Hz
delay(10);
}
void setPCA9685Servo(int ch, int angle) {
int pulse = map(angle, 0, 180, SERVO_MIN, SERVO_MAX);
// PCA9685 的 pulse 單位: 1/4096 週期
// 50 Hz 週期 = 20 ms = 20000 us
// 4096 / 20000 * pulse_us
pwm.setPWM(ch, 0, pulse * 4096 / 20000);
}
void loop() {
// 控制通道 0~15
for (int ch = 0; ch < 16; ch++) {
setPCA9685Servo(ch, 90); // 全部置中
}
delay(2000);
for (int ch = 0; ch < 16; ch++) {
setPCA9685Servo(ch, 0);
}
delay(2000);
}
ESP-IDF LEDC API
// ESP-IDF 伺服馬達控制
#include "driver/ledc.h"
#define SERVO_PIN 13
#define SERVO_CHANNEL LEDC_CHANNEL_0
#define SERVO_TIMER LEDC_TIMER_0
#define SERVO_FREQ 50
#define SERVO_RES LEDC_TIMER_16_BIT
void setup() {
ledc_timer_config_t timer_conf = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.duty_resolution = SERVO_RES,
.timer_num = SERVO_TIMER,
.freq_hz = SERVO_FREQ,
.clk_cfg = LEDC_AUTO_CLK
};
ledc_timer_config(&timer_conf);
ledc_channel_config_t ch_conf = {
.gpio_num = SERVO_PIN,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = SERVO_CHANNEL,
.timer_sel = SERVO_TIMER,
.duty = 0,
.hpoint = 0
};
ledc_channel_config(&ch_conf);
}
void setServo(int angle) {
uint32_t pulse = map(angle, 0, 180, 500, 2500); // us
uint32_t duty = pulse * 65535 / 20000;
ledc_set_duty(LEDC_LOW_SPEED_MODE, SERVO_CHANNEL, duty);
ledc_update_duty(LEDC_LOW_SPEED_MODE, SERVO_CHANNEL);
}
電源設計注意事項
| 伺服數量 | 建議電源 | 備註 |
|---|---|---|
| 1~2 顆 | 5V 1A 手機充電器 | 可直接從 USB 取電 |
| 3~6 顆 | 5V 3A 變壓器 | 使用獨立穩壓模組 |
| 7~12 顆 | 5V 5A 以上 | 需使用 5V 10A 電源 + 大電容 |
| 12+ 顆或 PCA9685 | 5V 10A+ / 6V 電池 | 需使用 LM2596 降壓穩壓 |
實務注意事項
- 獨立電源:伺服馬達不可從 ESP32 3.3V 取電,需獨立 5V 2A+ 電源並共地
- 大電容:伺服馬達啟動瞬間電流很大,電源端並聯 470~1000 uF 電解電容
- 訊號電平:ESP32 3.3V PWM 訊號對 SG90 完全相容(SG90 邏輯閾值約 1.5V)
- 死區:SG90 有約 2~5 us 的死區,角度控制精度約 0.2~0.5deg
- 抖動:PWM 頻率必須精確 50 Hz,頻率飄移會造成伺服抖動
- 保護:堵轉時持續供電會燒毀馬達或驅動晶片,建議加入電流檢測
- 金屬齒輪:SG90 為塑膠齒輪,頻繁使用建議更換 MG90S(金屬齒輪)
- 波特率限制:Servo.write() 內部使用 50 Hz 更新,每 20 ms 才能改變一次角度
常見問題
| 問題 | 原因 | 解法 |
|---|---|---|
| 伺服不動作 | 電源不足或 PWM 頻率錯誤 | 確認 5V 電源,確認 50 Hz |
| 伺服抖動 | PWM 不穩定或電源雜訊 | 加大電容,使用 LEDC 硬體 PWM |
| 角度不準 | 脈衝寬度偏差 | 使用 writeMicroseconds() 微調 |
| ESP32 重啟 | 伺服啟動電流拉低 3.3V | 獨立電源 + 共地 + 大電容 |
| Servo.h 編譯錯誤 | ESP32 Servo 函式庫衝突 | 使用 ESP32Saver/ServoESP32 或 LEDC API |
| 多軸不同步 | 多個 Servo 物件延遲 | 使用 LEDC 硬體通道同步 |
總結
ESP32 控制伺服馬達有從最簡的 Servo 函式庫到底層 LEDC API 的多種選擇。對於大多數應用,Servo 函式庫已經足夠;但需要精確同步或多軸控制時,LEDC 硬體 API 提供更好的效能和靈活性。
選型參考:
- 1~2 顆簡易控制:Servo 函式庫(寫入角度,自動管理 PWM)
- 4~8 軸專案:LEDC 底層 API(精確同步,硬體輸出)
- 8~16 軸以上:PCA9685 I2C 擴展(只需 2 條 I2C 線)
- 工業級 / 大扭力:改用 MG996R 或 Dynamixel 智慧伺服(數位通訊)
文章評論