0x6A Logbook

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

ESP32 伺服馬達 SG90 控制完整教學:PWM 脈衝控制、LEDC 多軸同步、平滑運動與梯形加減速

2026 年 6 月 23 日 728點熱度 0人點贊 0條評論

ESP32 伺服馬達 SG90 控制完整教學封面

ESP32 伺服馬達 SG90 控制:PWM 脈衝控制與多軸管理

如果你手上有一顆 SG90,想用 ESP32 讓它老老實實轉到你要的角度,那你來對地方了。這篇我打算用「邊做邊講」的方式,把 ESP32 控制伺服馬達這件事從頭拆開:訊號怎麼送、角度怎麼換算、多軸怎麼同步、動作怎麼變順,最後再談那個最容易被忽略、也最常把專案搞死的地方 —— 電源。

先講一個事實: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

有兩個數字值得你先圈起來:一個是 0.12 sec/60deg,這決定了你的動作至少要花多久才做得完;另一個是 堵轉扭力 1.8 kg.cm,它決定了你的電源要多大顆。

PWM 控制原理

伺服馬達的控制原理其實簡單到有點可愛:每隔 20 ms(也就是 50 Hz)送一個脈衝,脈衝的寬度決定轉到幾度。就這麼一件事,沒有什麼編碼或封包要處理。

SG90 PWM 控制時序:20 ms 週期與脈衝寬度

上圖是同一顆馬達在三種角度下的訊號長相。注意看:週期永遠是 20 ms 不變,變的只有脈衝有多寬。這也解釋了為什麼 PWM 頻率飄掉會直接讓伺服抖動 —— 週期一歪,馬達解讀到的角度就歪。

角度與脈衝寬度的對應關係是線性的:

角度 脈衝寬度 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%

SG90 角度 vs PWM 脈衝寬度對應曲線

把它畫成圖就是一條直線。所以你只要記住中間點:1.5 ms 對應 90 度,之後每多 1 度就多約 5.5 us 的脈衝寬度。關鍵公式如下:

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

硬體接線

ESP32 多軸伺服馬達接線架構

接線的部分,訊號線最單純:ESP32 的 GPIO 直接接到伺服馬達的訊號線就好,3.3V 的邏輯準位對 SG90 完全夠用(它的邏輯閾值大約 1.5V)。

重要:伺服馬達不可以從 ESP32 的 3.3V 取電。

SG90 在堵轉時電流可以衝到 750 mA,這遠遠超過 ESP32 板上 3.3V 穩壓器的輸出能力。比較常見的症狀是:程式看起來沒問題,但馬達一動 ESP32 就重開機 —— 那就是 3.3V 被拉垮了。正確做法是給伺服馬達一顆獨立的 5V 2A+ 電源,並且把 ESP32 的 GND 和電源的 GND 接在一起(共地)。訊號可以參考、電源要分家,這是整個專案最重要的紀律。

ESP32 LEDC PWM 硬體

訊號要從哪裡生出來?ESP32 內建了 LEDC(LED Control)PWM 控制器,顧名思義原本是拿來調 LED 亮度的,但它的精度跟彈性用來驅動伺服馬達剛剛好:

  • 8 通道(可擴展到 16 通道)
  • 20-bit 解析度(可設定 1~20 bit)
  • 硬體自動輸出(不佔用 CPU)
  • 可設定頻率(1 Hz ~ 40 MHz)
  • 3 組時基(每組共用頻率,各通道獨立佔空比)

最後那一點是重點:同一組時基下面的通道,波形是由同一個計時器產生,所以它們天然同步。多軸要對齊動作時,這就是你不用自己寫排程的原因。

Arduino 程式設計

接下來換寫程式。從最偷懶的寫法開始,再一層一層往下挖。

使用 Servo 函式庫(最簡易)

如果你只是想讓馬達轉起來,用 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);
}

這裡有個很容易被忽略的小細節:write() 內部只以 50 Hz 更新,也就是每 20 ms 才能改變一次角度。所以那一行 delay(15) 差不多就是 SG90 的物理極限,再快也追不上。

LEDC 底層 API(精確控制)

需要精確控制的時候,就得自己接底層的 LEDC API,把角度換成脈衝、再換成 duty:

// 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);
    }
}

關鍵就在 pulseToDuty():16-bit 的 duty 是 0~65535 對應整個 20,000 us 週期,所以 1500 us 的脈衝算出來大約是 4915。這個數字背下來不吃虧,之後看波形或除錯都會用到。

多軸伺服同步控制

ESP32 LEDC 四通道硬體同步輸出時序

圖中四個通道由同一組時基驅動,所以四個脈衝是同時起跑的,差別只在各自的高準位持續多久。這代表你可以放心地說「四顆馬達同時出發」,而不會出現第一顆已經轉完、第四顆才剛起步的窘境。

// 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);
}

要注意 setAngle() 是「立刻跳到目標角度」。馬達沒有情緒,但你會看到它甩頭。想要優雅一點,就得往下看平滑運動。

平滑運動(梯形加減速)

伺服馬達三種移動策略的位置曲線

上圖把三種做法攤在一起看。灰色那條是直接跳躍 —— 位置瞬間從 0 跳到 180,機構會承受全部的加速度。橘色那條是階梯式插值,每度停一下,位置是漂亮直線,但速度在頭尾是硬切上去的,所以接近目標時常常會抖一下。綠色那條是梯形加減速,起步慢、中段快、收尾慢,位置曲線是 S 形,動作最順。

// 階梯式插值 — 避免伺服馬達抖動
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);
        }
    }
}

實務上的經驗值:把加速與減速各留行程的三分之一、每度延遲 15~20 ms 起跳,大多數 SG90 就不會再甩頭了。

PCA9685 I2C 擴展(32+ 通道)

當軸數多起來,ESP32 的 GPIO 跟通道就開始不夠用。這時候可以外掛 PCA9685 這顆 16 通道的 PWM 驅動 IC,只要兩條 I2C 線就能再開 16 路,而且可以串接多顆(最多 62 顆,等於 992 顆伺服)。

// 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);
}

PCA9685 的計算邏輯跟 LEDC 一樣,只是單位換成 1/4096 週期,而且它有自己的震盪器,記得先校正頻率,不然 16 顆馬達的角度會集體偏移。

ESP-IDF LEDC API

如果你是用 ESP-IDF 而不是 Arduino 框架,邏輯完全一樣,只是把 API 換成 C 版本的設定結構:

// 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);
}

這裡很容易忘的一步是 ledc_update_duty():duty 設完要推一把才會生效,只寫 ledc_set_duty() 的話馬達完全不動。

電源設計注意事項

伺服數量與建議電源電流對照

電源這件事,用一張表講最快:

伺服數量 建議電源 備註
1~2 顆 5V 1A 手機充電器 可直接從 USB 取電
3~6 顆 5V 3A 變壓器 使用獨立穩壓模組
7~12 顆 5V 5A 以上 需使用 5V 10A 電源 + 大電容
12+ 顆或 PCA9685 5V 10A+ / 6V 電池 需使用 LM2596 降壓穩壓

你可能會覺得一顆 9 公克的馬達能吃多少電?答案是:靜止時幾乎不吃,但一起動就是幾百毫安在跳。所以電源要用「瞬間峰值」去選,不是用平均值。

實務注意事項

  • 獨立電源:伺服馬達不可從 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,再到外掛 PCA9685。大多數應用用 Servo 函式庫就夠了;但當你需要精確同步或多軸控制時,LEDC 硬體 API 的效能與彈性會讓事情簡單很多。

ESP32 伺服馬達控制方式選型決策

把上圖的決策流程攤成文字,選型參考就是這四句話:

  • 1~2 顆簡易控制:Servo 函式庫(寫入角度,自動管理 PWM)
  • 4~8 軸專案:LEDC 底層 API(精確同步,硬體輸出)
  • 8~16 軸以上:PCA9685 I2C 擴展(只需 2 條 I2C 線)
  • 工業級 / 大扭力:改用 MG996R 或 Dynamixel 智慧伺服(數位通訊)

至於我自己的習慣:先確認電源規格,再決定控制方式。因為馬達會不會動是電源決定的,動得好不好看才是程式決定的 —— 順序顛倒過來,你會多花很多時間在除錯上面。

標籤: 教學
最後更新: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