Arduino / ESP32

PWM trên ESP32 với LEDC - điều khiển tốc độ động cơ và độ sáng LED

7 phút đọc05/07/2026HocLapTrinhRobot
PWM trên ESP32 với LEDC - điều khiển tốc độ động cơ và độ sáng LED

ESP32 không có analogWrite - thay bằng hệ thống LEDC 16 kênh. Hướng dẫn dùng ledcAttach, ledcWrite điều khiển động cơ, LED và servo kèm code mẫu. Trong bài viết này, chúng ta sẽ cùng tìm hiểu chi tiết và ứng dụng thực tế trong robot.

LEDC là gì? Vì sao ESP32 không dùng analogWrite?

Trên Arduino Uno, muốn LED sáng mờ hay động cơ quay chậm ta gọi analogWrite(pin, 128). Chuyển sang ESP32, dòng lệnh quen thuộc này không tồn tại trong nhiều phiên bản core — thay vào đó ESP32 cung cấp hệ thống LEDC (LED Control): một khối phần cứng PWM chuyên dụng với 16 kênh độc lập, tần số lên tới 40 MHz và độ phân giải tùy chỉnh đến 16-bit.

LEDC mạnh hơn analogWrite truyền thống ở ba điểm:

  • Chọn được tần số: động cơ cần 1-20 kHz, servo cần đúng 50 Hz, LED cần trên 200 Hz để mắt không thấy nháy — LEDC đáp ứng tất cả, còn analogWrite của Uno bị cố định ~490/980 Hz
  • Chọn được độ phân giải: 8-bit (0-255) cho tương thích code cũ, 12-bit (0-4095) cho điều khiển mượt
  • 16 kênh song song: đủ cho 4 động cơ, 4 servo và dải LED cùng lúc

Nhắc lại nguyên lý PWM: tín hiệu bật-tắt cực nhanh, tỉ lệ thời gian bật (duty cycle) quyết định công suất trung bình. Duty 50% nghĩa là động cơ nhận một nửa công suất. Khái niệm PWM tổng quát có trong bài PWM là gì ở mục Kiến thức — bài này tập trung vào cách dùng trên ESP32.

API LEDC theo phiên bản core

Gói board ESP32 (Arduino core) có hai thế hệ API — nguồn gây nhầm lẫn lớn nhất khi copy code trên mạng:

Core 3.x (2024 trở đi - khuyên dùng)

cpp
const int MOTOR = 25;

void setup() {
  // gắn chân, tần số 1000 Hz, độ phân giải 8-bit
  ledcAttach(MOTOR, 1000, 8);
}

void loop() {
  ledcWrite(MOTOR, 200);   // duty 200/255 ≈ 78%
}

Core 3.x quản lý kênh tự động — bạn chỉ làm việc với số chân.

Core 2.x (code cũ trên mạng)

cpp
const int MOTOR = 25;
const int CHANNEL = 0;

void setup() {
  ledcSetup(CHANNEL, 1000, 8);        // kênh 0, 1 kHz, 8-bit
  ledcAttachPin(MOTOR, CHANNEL);      // gắn chân vào kênh
}

void loop() {
  ledcWrite(CHANNEL, 200);            // ghi theo KÊNH, không phải chân
}

Nhận biết nhanh: thấy ledcSetupledcAttachPin là code core 2.x; thấy ledcAttach là core 3.x. Trộn hai kiểu trong một chương trình sẽ báo lỗi biên dịch.

Chọn tần số và độ phân giải đúng

Ứng dụngTần số khuyến nghịĐộ phân giảiGhi chú
Động cơ DC qua L298N1.000 - 5.000 Hz8-bitTrên 20 kHz L298N nóng, dưới 500 Hz động cơ hú
Động cơ DC qua DRV8833/TB66125.000 - 20.000 Hz8-10 bitDriver MOSFET chịu tần số cao, trên 20 kHz hết tiếng rít
LED đơn, dải LED1.000 - 5.000 Hz8-12 bitTrên 200 Hz mắt hết thấy nháy; quay video nên 1 kHz+
Servo SG90/MG996Rđúng 50 Hz12-16 bitDuty 2.5-12.5% tương ứng 0-180 độ
Còi buzzer thụ động100 - 5.000 Hz (đổi liên tục)8-bit duty 50%Đổi tần số tạo giai điệu

Ràng buộc phần cứng: tần số nhân với 2^độ_phân_giải không vượt quá 80 MHz. Ví dụ 16-bit tối đa ~1.2 kHz; muốn 20 kHz thì độ phân giải tối đa 12-bit.

Ví dụ 1 - Điều khiển tốc độ 2 động cơ xe robot

cpp
const int ENA = 25;   // PWM động cơ trái
const int ENB = 13;   // PWM động cơ phải
const int IN1 = 26, IN2 = 27, IN3 = 14, IN4 = 12;

void setup() {
  pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT);
  pinMode(IN3, OUTPUT); pinMode(IN4, OUTPUT);
  ledcAttach(ENA, 1000, 8);
  ledcAttach(ENB, 1000, 8);
}

void run(int speedLeft, int speedRight) {
  // speed: -255 (lùi hết) đến 255 (tiến hết)
  digitalWrite(IN1, speedLeft >= 0); digitalWrite(IN2, speedLeft < 0);
  digitalWrite(IN3, speedRight >= 0); digitalWrite(IN4, speedRight < 0);
  ledcWrite(ENA, abs(speedLeft));
  ledcWrite(ENB, abs(speedRight));
}

void loop() {
  run(200, 200);   delay(2000);  // tiến
  run(150, -150);  delay(600);   // xoay tại chỗ
  run(0, 0);       delay(1000);  // dừng
}

Mẹo thực tế: động cơ TT thường không quay khi duty dưới ~70/255 vì không thắng nổi ma sát — dải hữu dụng thực tế là 80-255. Robot rẽ mượt bằng cách giảm tốc một bên thay vì dừng hẳn.

Ví dụ 2 - LED thở (fade) bằng 12-bit

cpp
const int LED = 2;

void setup() {
  ledcAttach(LED, 5000, 12);   // 5 kHz, 0-4095
}

void loop() {
  for (int duty = 0; duty <= 4095; duty += 16) {
    ledcWrite(LED, duty);
    delay(2);
  }
  for (int duty = 4095; duty >= 0; duty -= 16) {
    ledcWrite(LED, duty);
    delay(2);
  }
}

Độ phân giải 12-bit cho hiệu ứng mượt hơn hẳn 8-bit ở vùng sáng yếu — khác biệt rõ khi làm đèn ngủ hay hiệu ứng nhịp thở.

Ví dụ 3 - Servo bằng LEDC thuần

Servo nhận xung 50 Hz với độ rộng 0.5-2.5 ms. Với LEDC 50 Hz 12-bit, chu kỳ 20 ms tương ứng 4096 đơn vị:

cpp
const int SERVO = 4;

void setup() {
  ledcAttach(SERVO, 50, 12);
}

void servoWrite(int angle) {
  // 0 độ = 0.5ms = 102/4096, 180 độ = 2.5ms = 512/4096
  int duty = map(angle, 0, 180, 102, 512);
  ledcWrite(SERVO, duty);
}

void loop() {
  servoWrite(0);    delay(1000);
  servoWrite(90);   delay(1000);
  servoWrite(180);  delay(1000);
}

Thực tế nên dùng thư viện ESP32Servo cho tiện, nhưng hiểu cách làm thuần giúp bạn debug khi servo giật hoặc lệch góc.

Lỗi thường gặp với LEDC

  • Lỗi biên dịch "ledcSetup was not declared": code viết cho core 2.x đang biên dịch trên core 3.x — chuyển sang ledcAttach theo mẫu ở trên
  • Động cơ rít khó chịu: tần số PWM nằm trong dải tai người nghe (dưới 15 kHz); tăng lên 20 kHz nếu driver hỗ trợ (DRV8833 được, L298N không nên)
  • ledcWrite không tác dụng: quên gọi ledcAttach trước, hoặc trên core 2.x đang ghi theo số chân trong khi API nhận số kênh
  • Duty max không đạt 100%: giá trị tối đa là 2^độ_phân_giải - 1 (255 với 8-bit, 4095 với 12-bit) — ghi vượt bị cắt
  • Servo giật khi động cơ chạy: sụt nguồn — servo phải có nguồn 5V riêng đủ dòng, không ăn chung 3V3 của bo

Câu hỏi thường gặp

analogWrite có dùng được trên ESP32 không?

Core 3.x đã bổ sung analogWrite giả lập trên nền LEDC cho tương thích, dùng được cho LED đơn giản. Nhưng với động cơ và servo vẫn nên dùng LEDC trực tiếp để kiểm soát tần số — analogWrite không cho chọn tần số theo từng chân một cách rõ ràng.

Chân nào của ESP32 xuất PWM được?

Mọi chân output đều xuất PWM qua LEDC (trừ nhóm input-only 34, 35, 36, 39 và nhóm cấm 6-11). Đây là lợi thế lớn so với Uno chỉ có 6 chân PWM đánh dấu ~.

Vì sao cùng duty 128 mà hai động cơ quay khác tốc độ?

Sai lệch cơ khí tự nhiên giữa hai động cơ (ma sát, từ trường). Xe robot muốn đi thẳng cần bù tốc độ (trừ hao bên nhanh) hoặc dùng encoder + PID — xem bài Encoder đo tốc độ ở mục Kiến thức.

PWM có làm hỏng LED hay động cơ không?

Không — PWM chính là cách điều khiển tiêu chuẩn. Lưu ý duy nhất: LED vẫn cần điện trở hạn dòng, và động cơ vẫn cần driver; PWM không thay thế các linh kiện bảo vệ đó.

Tài nguyên tải về

Tải bản PDF của bài viết để in giáo án hoặc gửi trước cho học viên.

Tải PDF
Chia sẻ bài viết