Hướng dẫn dùng PWM trên ESP32: điều chỉnh độ sáng LED bằng analogWrite và LEDC

Hướng dẫn tạo PWM trên ESP32 bằng Arduino IDE: analogWrite, ledcAttach và ledcWrite của gói esp32 3.x, chỉnh độ sáng LED bằng biến trở, chọn tần số và độ phân giải.

Chân GPIO của ESP32 chỉ xuất được hai mức 0 V và 3,3 V, vậy làm sao để LED sáng mờ hay động cơ quay chậm? Câu trả lời là PWM: bật tắt chân thật nhanh và thay đổi tỉ lệ thời gian bật. Bài viết giải thích PWM, hai cách tạo PWM trên ESP32 bằng Arduino IDE (analogWrite() và bộ LEDC), cách chỉnh độ sáng LED bằng biến trở và cách chọn tần số, độ phân giải.

1. PWM là gì?

PWM (Pulse Width Modulation, điều chế độ rộng xung) là tín hiệu xung vuông có tần số cố định, trong đó phần thời gian ở mức cao của mỗi chu kỳ thay đổi được. Tỉ lệ này gọi là duty. Duty càng lớn thì điện áp trung bình trên chân càng cao và LED càng sáng.

Dạng xung PWM với duty 0, 25, 50, 75 và 100 phần trăm, kèm giá trị 8 bit và điện áp trung bình tương ứng trên chân 3,3 V
Duty quyết định điện áp trung bình trên chân GPIO (hình vẽ minh họa, số liệu tính theo công thức).

Khi tần số đủ cao (từ khoảng 100 Hz trở lên), mắt không còn thấy LED nhấp nháy mà chỉ thấy độ sáng thay đổi.

2. PWM trên ESP32: bộ LEDC

ESP32 tạo PWM bằng khối phần cứng tên là LEDC (LED Control). Khác với Arduino Uno chỉ có vài chân PWM cố định, ESP32 cho phép:

  • Xuất PWM trên hầu hết các chân GPIO có chức năng ngõ ra (không dùng được GPIO34–39 vì chỉ là ngõ vào).
  • Dùng tới 16 kênh PWM cùng lúc.
  • Tự chọn tần số và độ phân giải. Độ phân giải là số bit dùng để chia duty: 8 bit cho 256 mức (0–255), 12 bit cho 4096 mức (0–4095).

Tần số và độ phân giải ràng buộc lẫn nhau: độ phân giải càng cao thì tần số tối đa càng thấp.

Độ phân giải PWM và tần số tối đa tương ứng trên ESP32
Độ phân giảiKhoảng giá trị dutyTần số tối đa
8 bit0–255Khoảng 312 kHz
10 bit0–1023Khoảng 78 kHz
12 bit0–4095Khoảng 19,5 kHz
13 bit0–8191Khoảng 9,7 kHz
16 bit0–65535Khoảng 1,2 kHz

Các giá trị trên tính theo công thức tần số tối đa = 80 MHz / 2số bit.

3. Linh kiện cần chuẩn bị

4. Sơ đồ nối dây

Nối LED và biến trở với ESP32 DevKit V1
Linh kiệnESP32 DevKit V1Ghi chú
Điện trở 220 Ω nối tiếp chân dài (+) của LEDD18 / GPIO18Chân xuất PWM.
Chân ngắn (−) của LEDGNDMass chung.
Chân giữa của biến trởD34 / GPIO34Ngõ vào analog, thuộc nhóm ADC1.
Hai chân ngoài của biến trở3V3 và GNDĐổi chỗ hai chân này chỉ làm đảo chiều vặn.
Sơ đồ nối ESP32 DevKit V1 với LED qua điện trở 220 ohm ở chân D18 và biến trở 10 kΩ ở chân D34
Sơ đồ nối LED và biến trở với ESP32 DevKit V1 (hình vẽ minh họa).

Điện trở 220 Ω giới hạn dòng qua LED ở khoảng 6 mA, an toàn cho chân GPIO. Không nối LED thẳng vào chân GPIO mà không có điện trở. Chỉ cấp cho biến trở 3,3 V: ngõ vào analog của ESP32 không chịu được 5 V.

5. Cách 1: dùng analogWrite()

Đây là cách ngắn nhất và quen thuộc với người đã dùng Arduino. Chương trình cho LED sáng dần rồi tối dần:

/*
  PYWORLD.VN - ESP32 PWM bằng analogWrite(): LED sáng dần rồi tối dần
  LED: GPIO18 -> điện trở 220 ohm -> chân dài của LED; chân ngắn của LED -> GND
  Không cần cài thêm thư viện (gói esp32 phiên bản 3.x)
*/

#define LED_PIN 18

void setup() {
  // analogWrite() dùng cấu hình mặc định: độ phân giải 8 bit (0-255), tần số 1000 Hz
}

void loop() {
  // Sáng dần
  for (int doSang = 0; doSang <= 255; doSang++) {
    analogWrite(LED_PIN, doSang);
    delay(8);
  }

  // Tối dần
  for (int doSang = 255; doSang >= 0; doSang--) {
    analogWrite(LED_PIN, doSang);
    delay(8);
  }
}

Chọn board DOIT ESP32 DEVKIT V1 rồi nạp chương trình. analogWrite(chân, giá trị) dùng độ phân giải 8 bit và tần số 1000 Hz; giá trị 0 là tắt hẳn, 255 là sáng nhất. Có thể đổi hai thông số này bằng analogWriteResolution(chân, số bit) và analogWriteFrequency(chân, tần số).

6. Cách 2: dùng LEDC và chỉnh độ sáng bằng biến trở

Các hàm LEDC cho phép chọn rõ tần số và độ phân giải ngay từ đầu. Chương trình dưới đây dùng PWM 12 bit, trùng với độ phân giải 12 bit của analogRead(), nên giá trị đọc từ biến trở được dùng thẳng làm duty:

/*
  PYWORLD.VN - ESP32 PWM bằng LEDC: chỉnh độ sáng LED bằng biến trở
  LED: GPIO18 -> điện trở 220 ohm -> chân dài của LED; chân ngắn của LED -> GND
  Biến trở 10 kΩ: hai chân ngoài nối 3V3 và GND, chân giữa nối GPIO34
  Không cần cài thêm thư viện (gói esp32 phiên bản 3.x)
*/

#define LED_PIN       18
#define BIEN_TRO_PIN  34     // Chân ADC1, chỉ dùng làm ngõ vào

#define TAN_SO_HZ     5000   // Tần số PWM
#define DO_PHAN_GIAI  12     // 12 bit: duty từ 0 đến 4095

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

  // Gắn chân LED vào bộ PWM với tần số và độ phân giải đã chọn
  if (!ledcAttach(LED_PIN, TAN_SO_HZ, DO_PHAN_GIAI)) {
    Serial.println("Khong cau hinh duoc PWM! Kiem tra tan so va do phan giai.");
    while (true) delay(1000);
  }
}

void loop() {
  int giaTri = analogRead(BIEN_TRO_PIN);   // 0-4095, cùng 12 bit với PWM
  ledcWrite(LED_PIN, giaTri);              // Dùng thẳng giá trị đọc được làm duty

  Serial.printf("Bien tro: %4d  ->  duty: %5.1f %%\n", giaTri, giaTri * 100.0 / 4095);
  delay(100);
}

Mở Serial Monitor ở 115200 baud và vặn biến trở: LED sáng dần theo góc vặn.

Hình minh họa Serial Monitor in giá trị biến trở từ 0 đến 4095 và duty PWM tương ứng từ 0 đến 100 phần trăm
Kết quả mong đợi khi vặn biến trở từ đầu này sang đầu kia (hình minh họa theo định dạng in của chương trình).

7. Giải thích các lệnh chính

  • ledcAttach(LED_PIN, TAN_SO_HZ, DO_PHAN_GIAI): gắn một chân vào bộ LEDC với tần số 5000 Hz và độ phân giải 12 bit. ESP32 tự chọn kênh còn trống. Hàm trả về false nếu tần số và độ phân giải không đi được với nhau.
  • ledcWrite(LED_PIN, duty): đặt duty cho chân đó. Với 12 bit, duty chạy từ 0 đến 4095.
  • analogRead(BIEN_TRO_PIN): đọc điện áp ở chân giữa biến trở, kết quả từ 0 đến 4095.
  • giaTri * 100.0 / 4095: đổi duty sang phần trăm để dễ theo dõi.

Mắt người cảm nhận độ sáng không đều theo duty: từ 0 lên 25 % thấy sáng lên rất nhanh, còn từ 75 % lên 100 % gần như không khác. Độ phân giải 12 bit cho nhiều mức ở vùng duty thấp nên LED mờ dần mượt hơn 8 bit.

8. Biến thể: để phần cứng tự làm mờ dần

Bộ LEDC có sẵn chức năng thay đổi duty dần theo thời gian. Lệnh ledcFade() chỉ cần duty đầu, duty cuối và thời gian; phần cứng tự làm phần còn lại, chương trình không phải chạy vòng lặp:

/*
  PYWORLD.VN - ESP32 PWM bằng LEDC: để phần cứng tự làm LED sáng dần, tối dần (ledcFade)
  LED: GPIO18 -> điện trở 220 ohm -> chân dài của LED; chân ngắn của LED -> GND
  Không cần cài thêm thư viện (gói esp32 phiên bản 3.x)
*/

#define LED_PIN        18
#define TAN_SO_HZ      5000
#define DO_PHAN_GIAI   12
#define DUTY_LON_NHAT  4095
#define THOI_GIAN_MS   2000

void setup() {
  ledcAttach(LED_PIN, TAN_SO_HZ, DO_PHAN_GIAI);
}

void loop() {
  // Sáng dần trong 2 giây; lệnh trả về ngay, phần cứng tự thay đổi duty
  ledcFade(LED_PIN, 0, DUTY_LON_NHAT, THOI_GIAN_MS);
  delay(THOI_GIAN_MS + 200);

  // Tối dần trong 2 giây
  ledcFade(LED_PIN, DUTY_LON_NHAT, 0, THOI_GIAN_MS);
  delay(THOI_GIAN_MS + 200);
}

Vì ledcFade() trả về ngay, trong lúc LED đang đổi độ sáng chương trình vẫn có thể làm việc khác thay cho lệnh delay().

9. Chọn tần số, độ phân giải và điều khiển tải lớn hơn

  • LED: tần số 1–5 kHz, độ phân giải 8 đến 12 bit.
  • Động cơ DC qua mạch công suất: thường dùng 5–20 kHz để giảm tiếng rít.
  • Servo RC: tần số 50 Hz, độ phân giải 16 bit; xung điều khiển 1–2 ms tương ứng duty khoảng 3277–6554.
  • Nhiều chân cùng lúc: gọi ledcAttach() cho từng chân. Các chân dùng cùng tần số và độ phân giải được ghép chung một bộ định thời. Mỗi nhóm 8 kênh có 4 bộ định thời, nên trong 8 chân đầu tiên dùng được tối đa 4 cấu hình tần số khác nhau.
  • Tải lớn hơn một LED: mỗi chân GPIO chỉ nên cấp dòng dưới khoảng 20 mA. Dải LED, động cơ hay quạt phải điều khiển qua MOSFET hoặc mạch công suất, chân GPIO chỉ đưa tín hiệu PWM.
  • Không dùng PWM với relay cơ: module relay chỉ để đóng ngắt, tiếp điểm cơ khí không theo kịp xung PWM.

10. Xử lý lỗi thường gặp

  • Lỗi biên dịch "ledcSetup was not declared in this scope" hoặc "ledcAttachPin was not declared": chương trình viết cho gói esp32 phiên bản 2.x. Từ phiên bản 3.0, hai lệnh ledcSetup() và ledcAttachPin() được gộp thành ledcAttach(chân, tần số, độ phân giải), và ledcWrite() nhận số chân thay cho số kênh.
  • LED không sáng: kiểm tra chiều LED (chân dài nối về phía điện trở và GPIO18), điện trở và dây GND. Thử đổi LED_PIN thành 2 để dùng LED có sẵn trên mạch DevKit V1.
  • LED chỉ bật hoặc tắt, không mờ dần: giá trị duty vượt quá khoảng của độ phân giải, ví dụ ghi 4095 khi đang dùng 8 bit.
  • Serial Monitor báo "Khong cau hinh duoc PWM!": tần số quá cao so với độ phân giải đã chọn. Xem bảng ở mục 2 rồi giảm tần số hoặc giảm số bit.
  • Số đọc biến trở nhảy lung tung: chân giữa biến trở chưa nối chắc, hoặc đang dùng chân thuộc nhóm ADC2 trong khi bật Wi-Fi. Dùng các chân ADC1 là GPIO32–39.
  • Vặn gần hết về một đầu thì độ sáng không đổi nữa: ADC của ESP32 có vùng chết ở hai đầu dải đo: dưới khoảng 0,15 V đọc ra 0, và gần 3,3 V đã đọc ra 4095. Đây là đặc tính của chip.
  • Không nạp được chương trình: dùng cáp USB có truyền dữ liệu, chọn đúng cổng COM và giữ nút BOOT khi IDE dừng ở dòng Connecting...

Xem thêm: ESP32 điều khiển module relay 4 kênh, đo điện áp 16 bit bằng ADS1115 với ESP32 khi cần đọc analog chính xác hơn và bắt đầu MicroPython trên ESP32 với Thonny.


Tin tức liên quan

Hướng dẫn làm cân điện tử với ESP32, HX711 và load cell trên Arduino IDE
Hướng dẫn làm cân điện tử với ESP32, HX711 và load cell trên Arduino IDE

7 Lượt xem

Hướng dẫn nối load cell với module HX711 và ESP32 DevKit V1, gá load cell đúng cách, hiệu chuẩn bằng vật đã biết khối lượng và viết chương trình cân trên Arduino IDE.

Hướng dẫn làm đồng hồ Internet với ESP32, NTP và màn hình OLED 0.96 inch
Hướng dẫn làm đồng hồ Internet với ESP32, NTP và màn hình OLED 0.96 inch

5 Lượt xem

Hướng dẫn ESP32 lấy giờ Việt Nam từ Internet bằng NTP trên Arduino IDE, in ra Serial Monitor và làm đồng hồ hiển thị thứ, ngày, giờ trên màn hình OLED 0.96 inch SSD1306.

Hướng dẫn dùng MQTT với ESP32: gửi dữ liệu và điều khiển relay bằng PubSubClient
Hướng dẫn dùng MQTT với ESP32: gửi dữ liệu và điều khiển relay bằng PubSubClient

5 Lượt xem

Hướng dẫn dùng MQTT với ESP32 trên Arduino IDE: broker, topic, publish, subscribe, cài Mosquitto và PubSubClient, gửi dữ liệu và điều khiển module relay qua mạng nội bộ.

Hướng dẫn điều khiển relay bằng điện thoại qua Bluetooth với ESP32
Hướng dẫn điều khiển relay bằng điện thoại qua Bluetooth với ESP32

5 Lượt xem

Hướng dẫn dùng Bluetooth Classic của ESP32 trên Arduino IDE: ghép đôi với điện thoại Android, gửi nhận chữ bằng BluetoothSerial và điều khiển module relay bằng lệnh ON, OFF.

Hướng dẫn nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE
Hướng dẫn nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE

4 Lượt xem

Hướng dẫn bật OTA trên ESP32 bằng thư viện ArduinoOTA có sẵn, nạp chương trình qua Wi-Fi từ Arduino IDE 2.x, đặt mật khẩu và các quy tắc để không mất khả năng cập nhật từ xa.

Hướng dẫn dùng màn hình OLED 0.96 inch SSD1306 với Arduino Nano
Hướng dẫn dùng màn hình OLED 0.96 inch SSD1306 với Arduino Nano

5 Lượt xem

Hướng dẫn nối màn hình OLED 0.96 inch SSD1306 (I2C) với Arduino Nano, cài thư viện Adafruit, hiện chữ nhiều cỡ, vẽ thanh mức theo điện áp và lưu ý về bộ nhớ RAM.

Hướng dẫn đọc ADC trên ESP32 và đo điện áp pin 18650 bằng cầu phân áp
Hướng dẫn đọc ADC trên ESP32 và đo điện áp pin 18650 bằng cầu phân áp

5 Lượt xem

Hướng dẫn đọc điện áp analog bằng ADC của ESP32 trên Arduino IDE, dùng analogReadMilliVolts, làm cầu phân áp đo pin Li-ion 18650 và ước lượng phần trăm pin còn lại.

Hướng dẫn dùng Deep Sleep trên ESP32: tiết kiệm pin, đánh thức bằng timer và nút nhấn
Hướng dẫn dùng Deep Sleep trên ESP32: tiết kiệm pin, đánh thức bằng timer và nút nhấn

5 Lượt xem

Hướng dẫn đưa ESP32 vào Deep Sleep trên Arduino IDE để tiết kiệm pin: các chế độ nguồn, đánh thức bằng timer và nút nhấn, giữ biến trong bộ nhớ RTC và ước tính thời lượng pin.


Bình luận
Đã thêm vào giỏ hàng