ESP32 (EP.5) ESP32 กับงาน PWM

วันนี้จะขอเสนอการใช้งาน ESP32 ในงาน PWM …. PWM ย่อมาจาก Pulse Width Modulation มันคือการกำเนิดลูกคลื่นสี่เหลี่ยมในความถี่ต่าง ๆ และสามารถเปลี่ยนแปลงค่าของ % duty cycle ได้  การกำเนิด PWM แบบนี้มีประโยชน์หลายงานแล้วแต่งานที่เราต้องการจะทำครับ  โดยหลักเลยก็คือการควบคุมความเร็วของมอเตอร์แบบ BLDC และยังสามารถใช้ในการควบคุมความสว่างของหลอดไฟขนาดเล็กอย่างเช่น LED ได้ด้วยการป้อน PWM เข้าไปใน LED ด้วย duty cycle ที่เปลี่ยนแปลงระหว่างช่วง 10 – 90% ซึ่งจะทำให้ LED สว่างน้อยถึงมาก

ก่อนอื่นมาดูลักษณะของรูปคลื่น PWM ก่อนว่าลักษณะของ duty cycle ว่ามันจะเป็นอย่างไร  จากภาพนี้จะเห็นว่ารูปคลื่นสี่เหลี่ยมจะมีความกว้างช่วง ON และ OFF ใน 1 ช่วงคลื่น  ถ้าช่วง ON มีค่าน้อยก็หมายความว่า % ของ duty cycle มีค่าต่ำ  และหากช่วง ON กว้างและช่วง OFF แคบนิดเดียวก็คือรูปคลื่นนั้นมี % ของ duty cycle สูงครับ


ใน ESP32 จะมี hardware ที่ทำหน้าที่กำเนิด PWM โดยเฉพาะ  นั่นก็หมายความว่าในการกำเนิด PWM จะไม่กินกำลัง CPU ของ ESP32 เลยเพราะมันเป็น dedicated hardware ซึ่งจะแบ่งเป็น 2 ส่วนก็คือ
🛑  LEDC (LED Controller)  เป็นส่วนของ hardware ที่ออกแบบสำหรับใช้งานแบบ basic สามารถนำไปคุมความสว่างของ LED ก็ได้ หรือกำเนิดคลื่นสี่เหลี่ยมเพื่อใช้งานอเนกประสงค์ใด ๆ ก็ได้โดยสามารถปรับ duty cycle ได้อย่างอิสระ  อย่างในโปรเจคที่ผมจะสาธิตด้านล่างนี้สามารถกำเนิดได้กว้างมากถึง 5 Hz – 1 MHz และปรับ duty cycle ได้กว้างในช่วง 5 – 95% โดยทำงานผ่าน web dashboard

🛑 MCPWM (Motor Control Pulse Width Modulator)  ส่วนนี้จะทำงานค่อนข้างซับซ้อนเนื่องจากออกแบบให้ควบคุมมอเตอร์แบบ 3 เฟส  ดังนั้นจึงให้ output ได้ถึง 3 ชุดและมีขารองรับการป้อนกลับจากตัวมอเตอร์เพื่อสร้าง timing ที่แม่นยำ


ต่อไปจะว่าด้วยข้อจำกัดของในการกำเนิด PWM …. เนื่องจาก ESP32 ทำงานด้วยสัญญาณนาฬิกา clock ความถี่ 80 MHz จาก APB (Advanced Peripheral Bus)  ดังนั้นจึงมีข้อจำกัดอยู่เล็กน้อยในการกำเนิดที่ความถี่สูงระดับ 1 MHz หรือประมาณนี้  สิ่งที่ว่านี้ก็คือ Resolution ของ duty cycle

จาก clock 80 MHz ของ ESP32 เมื่อ LEDC ทำงานภายในตัวจะมี counter ภายใน  หากเรากำหนดให้สร้างความถี่ 1 MHz  ตัว counter ของ LEDC ก็จะสามารถนับได้เท่ากับ 80 ticks ซึ่งจำนวน 80 ticks ที่นับได้นี้ค่าของ resolution ก็จะมีค่าประมาณ 6 bit  ตามสมการ log นี้

และหากเรากำเนิดที่ความถี่ต่ำกว่านี้อย่างเช่น 40 KHz  ค่า resolution ก็จะประมาณ 10 bit

ค่า resolution ที่เป็นจำนวน bit นี้มันหมายความถึง step ของ duty cycle ที่สามารถทำได้ อย่างที่ความถี่ 1 MHz = resolution เท่ากับ 6 bit  ดังนั้น steps ของค่า duty cycle ที่จะเปลี่ยนแปลงได้ก็จะ = 26 = 64 steps  ก็คือ duty cycle 0 – 100% จะเริ่มต้นจาก (100/64) = 1.5625% → 3.125 → 4.6875  6.25  7.8125  9.375 …. 50  51.5625 ไปจนถึง 98.4375 %

และหากที่ความถี่ต่ำกว่านี้เช่น 40 KHz  ค่า resolution ก็จะประมาณ 10 bit และหาความถี่ต่ำลงไปอีกก็จะสามารถปรับ step ของ duty cycle ได้อย่างละเอียดมากกว่านี้อีกแต่ก็สูงสุดก็คือ 16 bit ครับ  0.09765625 → 0.1953125 → 0.29296875 → 0.390625 …. ซึ่งละเอียดมาก

ในคลิปที่ท่านเห็นด้านล่างนี้คือไฟวิ่งที่ออกแบบให้การวิ่งของ LED 5 ดวงนี้เป็นไปแบบ smooth fade นั่นก็คือใช้ PWM มาช่วยครับ โดยเปลี่ยนจากการกระพริบธรรมดาคือเปลี่ยนจาก logic 1 และ 0 ของ GPIO ให้เป็นกำเนิด PWM ออกมาจาก GPIO ทั้ง 5 ขา โดยในช่วงการเปลี่ยนการวิ่งระหว่าง LED แต่ละดวงก็ป้อน PWM ที่มี duty cycle เปลี่ยนแปลงระหว่าง 10 – 90% ทำให้ลักษณะของไฟวิ่งจะดูนุ่มนวลอย่างที่เห็นครับ


อุปกรณ์ที่ใช้มีเพียงเท่านี้

Code ทั้งหมดของไฟวิ่ง PWM

// Define Pins for both groups
const int pinsGroupA[] = {21, 38, 39}; // 3 LEDs (Simple ON/OFF running)
const int numGroupA = 3;

const int pinsGroupB[] = {4, 5, 6, 7, 8}; // 5 LEDs (Smooth PWM Fading)
const int numGroupB = 5;

const int potPin = 1;

// PWM Settings for ESP32-S3 (Core 3.x)
const int pwmFreq = 5000;       // 5 kHz PWM frequency
const int pwmResolution = 8;    // 8-bit resolution (0 - 255 brightness)
const int maxBrightness = 255;

// State Machine States
enum RunState {
  MODE_THREE_LEDS,
  MODE_FIVE_LEDS
};
RunState currentState = MODE_THREE_LEDS;

// Sequencer Variables
int currentLedIdx = 0;
int previousLedIdx = -1;
int cycleCount = 0;

unsigned long lastRunMillis = 0;
unsigned long lastFadeMillis = 0;
const int fadeInterval = 5; // Update fade every 5ms
float fadeProgress = 0.0;

void setup() {
  // Initialize Group A (3 LEDs) as standard digital outputs
  for (int i = 0; i < numGroupA; i++) {
    pinMode(pinsGroupA[i], OUTPUT);
    digitalWrite(pinsGroupA[i], LOW);
  }

  // Initialize Group B (5 LEDs) using the Core 3.x PWM API
  for (int i = 0; i < numGroupB; i++) {
    ledcAttach(pinsGroupB[i], pwmFreq, pwmResolution);
    ledcWrite(pinsGroupB[i], 0);
  }
  
  analogReadResolution(12); // 12-bit ADC (0-4095)
}

void loop() {
  unsigned long currentMillis = millis();

  // --- STATE 1: 3 LEDs Running at Fixed 3 Hz ---
  if (currentState == MODE_THREE_LEDS) {
    unsigned long runIntervalA = 1000 / 3; // Fixed 3 Hz frequency -> ~333ms per step

    if (currentMillis - lastRunMillis >= runIntervalA) {
      lastRunMillis = currentMillis;

      // Turn off previous LED
      if (previousLedIdx != -1) {
        digitalWrite(pinsGroupA[previousLedIdx], LOW);
      }
      
      // Turn on current LED
      digitalWrite(pinsGroupA[currentLedIdx], HIGH);

      // Track sequencing indices
      previousLedIdx = currentLedIdx;
      currentLedIdx++;

      // Check if one full cycle of 3 LEDs is complete
      if (currentLedIdx >= numGroupA) {
        currentLedIdx = 0;
        cycleCount++;

        // If 3 complete cycles are reached, reset and switch state
        if (cycleCount >= 3) {
          // Clean up Group A states
          digitalWrite(pinsGroupA[previousLedIdx], LOW);
         
          // Switch to Group B
          currentState = MODE_FIVE_LEDS;
          cycleCount = 0;
          currentLedIdx = 0;
          previousLedIdx = numGroupB - 1; // Prepare fading index for Group B
          fadeProgress = 0.0;
        }
      }
    }
  }

  // --- STATE 2: 5 LEDs Running with Smooth Fade + Variable POT Speed ---
  else if (currentState == MODE_FIVE_LEDS) {
    // Read POT to calculate variable frequency (1Hz to 5Hz)
    int potValue = analogRead(potPin);
    float frequency = map(potValue, 0, 4095, 100, 500) / 100.0;
    unsigned long runIntervalB = 1000 / frequency; // Dynamic step time based on frequency

    // A. Handle Smooth Fading Calculations (Every 5ms)
    if (currentMillis - lastFadeMillis >= fadeInterval) {
      lastFadeMillis = currentMillis;

      unsigned long timePassed = currentMillis - lastRunMillis;
      fadeProgress = (float)timePassed / (float)runIntervalB;
      if (fadeProgress > 1.0) fadeProgress = 1.0;

      int fadeInDuty = fadeProgress * maxBrightness;
      int fadeOutDuty = (1.0 - fadeProgress) * maxBrightness;

      // Apply smooth PWM fading to current and previous LEDs
      ledcWrite(pinsGroupB[currentLedIdx], fadeInDuty);
      ledcWrite(pinsGroupB[previousLedIdx], fadeOutDuty);
    }

    // B. Handle stepping to the next LED in Group B
    if (currentMillis - lastRunMillis >= runIntervalB) {
      lastRunMillis = currentMillis;

      // Ensure clean final values for step transition
      ledcWrite(pinsGroupB[previousLedIdx], 0);
      ledcWrite(pinsGroupB[currentLedIdx], maxBrightness);

      previousLedIdx = currentLedIdx;
      currentLedIdx++;

      // Check if one full cycle of 5 LEDs is complete
      if (currentLedIdx >= numGroupB) {
        currentLedIdx = 0;
        cycleCount++;

        // If 3 complete cycles are reached, reset and switch state back to Group A
        if (cycleCount >= 3) {
          // Clean up Group B states
          ledcWrite(pinsGroupB[previousLedIdx], 0);

          // Switch to Group A
          currentState = MODE_THREE_LEDS;
          cycleCount = 0;
          currentLedIdx = 0;
          previousLedIdx = -1; // Reset to safe initial value for Group A
        }
      }
      
      fadeProgress = 0.0; // Reset fade progress for the new step
    }
  }
}

ต่อไปคือ project จริงที่ใช้ประโยชน์สูงสุดจากของ PWM โดยออกแบบให้กำเนิดความถี่ในช่วงกว้างมากระหว่าง 5 Hz – 1 MHz และสามารถปรับ duty cycle ได้ช่วงกว้างระหว่าง 5 – 95% ครับและเพื่อความสะดวกก็ออกแบบให้รับค่าความถี่และ duty cycle จาก web dashboard ได้เลยดูรายละเอียดในคลิปครับ


Code ทั้งหมดของ PWM generator + web dashboard

#include <WiFi.h>
#include <WebServer.h>

// --- WiFi Credentials ---
const char* ssid = " SSID ของ Wi-Fi ท่าน";
const char* password = " password ของ Wi-Fi ท่าน ";

// --- Hardware Settings ---
const uint8_t PWM_PIN = 11;
uint32_t currentFreq = 1000; // Default 1 kHz
uint8_t currentDuty = 50;    // Default 50%

WebServer server(80);

// --- HTML/CSS/JS Dashboard ---
// Stored in program memory (PROGMEM) to save RAM
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ESP32-S3 PWM Controller</title>
  <style>
    body { font-family: Arial, sans-serif; text-align: center; background-color: #f4f4f9; padding: 20px; }
    .card { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); display: inline-block; max-width: 100%; overflow-x: hidden; }
    h2 { color: #333; }
    .slider-container { margin: 30px 0; }
    input[type=range] { width: 100%; max-width: 400px; }
    
    /* PC Specific styling: 1200px wide sliders */
    @media (min-width: 1024px) {
      input[type=range] { width: 1200px; max-width: 1200px; }
      .card { max-width: 1300px; }
    }
    slider
    .value-label { font-size: 1.2em; font-weight: bold; color: #0066cc; }
    input[type=number] { padding: 10px; font-size: 1em; width: 150px; text-align: center; border: 1px solid #ccc; border-radius: 5px; }
    button { padding: 10px 20px; font-size: 1em; background-color: #0066cc; color: white; border: none; border-radius: 5px; cursor: pointer; }
    button:hover { background-color: #004c99; }
  </style>
</head>
<body>
  <div class="card">
    <h2>ESP32-S3 PWM Dashboard</h2>
    
    <div class="slider-container">
      <h3>Frequency (Hz)</h3>
      <input type="range" id="freqSlider" min="1" max="1000000" value="1000" oninput="updateFreqFromSlider()">
      <br><br>
      <input type="number" id="freqInput" min="1" max="1000000" value="1000" oninput="updateFreqFromInput()">
      <button onclick="executeSend()">Send</button>
    </div>

    <hr>

    <div class="slider-container">
      <h3>Duty Cycle (%)</h3>
      <input type="range" id="dutySlider" min="5" max="95" step="5" value="50" oninput="onDutySlide()">
      <p class="value-label"><span id="dutyLabel">50</span>%</p>
    </div>
  </div>

  <script>
    let canSend = true;

    // Sync input box when slider moves and throttle network request
    function updateFreqFromSlider() {
      document.getElementById("freqInput").value = document.getElementById("freqSlider").value;
      throttleSend();
    }

    // Sync slider when input box changes (User types value)
    function updateFreqFromInput() {
      let val = document.getElementById("freqInput").value;
      // Clamp values between 1 and 1,000,000
      if(val < 1) val = 1;
      if(val > 1000000) val = 1000000;
      document.getElementById("freqSlider").value = val;
    }

    // Update duty label and throttle network request
    function onDutySlide() {
      document.getElementById("dutyLabel").innerText = document.getElementById("dutySlider").value;
      throttleSend();
    }

    // Ensures we only send HTTP GET every 1 second maximum while sliding
    function throttleSend() {
      if(canSend) {
        executeSend();
        canSend = false;
        setTimeout(() => { canSend = true; }, 1000); // 1000ms = 1 second
      }
    }

    // The actual HTTP GET request
    function executeSend() {
      let f = document.getElementById("freqInput").value;
      let d = document.getElementById("dutySlider").value;
      
      fetch(`/update?freq=${f}&duty=${d}`)
        .then(response => { console.log("PWM Updated"); })
        .catch(error => { console.error("Error updating PWM", error); });
    }
  </script>
</body>
</html>
)rawliteral";

// --- Function to update hardware PWM based on ESP32 Core v3.x API ---
// --- Function to update hardware PWM based on ESP32 Core v3.x API ---
void setPWM(uint32_t freq, uint8_t dutyPercent) {
  // 1. Calculate the maximum bit resolution for this frequency
  uint8_t resolution = log2(80000000 / freq);
  
  // Clamp resolution to a maximum of 14 bits for stability, and minimum of 1 bit
  if (resolution > 14) resolution = 14;
  if (resolution < 1) resolution = 1;

  // 2. THE FIX: Update frequency dynamically
  // ledcChangeFrequency returns 0 if the pin hasn't been attached yet.
  // So if it fails (like during the first run in setup()), we use ledcAttach instead.
  if (ledcChangeFrequency(PWM_PIN, freq, resolution) == 0) {
    ledcAttach(PWM_PIN, freq, resolution);
  }

  // 3. Convert the percentage (5-95%) into the raw hardware value based on resolution
  uint32_t max_duty_value = (1 << resolution) - 1;
  uint32_t raw_duty = (dutyPercent * max_duty_value) / 100;

  // 4. Write the value to the hardware
  ledcWrite(PWM_PIN, raw_duty);
  
  Serial.printf("Set PWM -> Freq: %d Hz | Duty: %d%% | Res: %d-bit | Raw Duty: %d/%d\n",
                freq, dutyPercent, resolution, raw_duty, max_duty_value);
}

void setup() {
  Serial.begin(115200);
  ledcSetClockSource(LEDC_USE_APB_CLK);
  // Initialize PWM with default values
  setPWM(currentFreq, currentDuty);

  // Connect to Wi-Fi
  Serial.print("Connecting to WiFi: ");
  Serial.println(ssid);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi Connected!");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

  // --- Web Server Routes ---
  
  // 1. Serve the Dashboard
  server.on("/", []() {
    server.send(200, "text/html", index_html);
  });

  // 2. Handle the HTTP GET request from JS (e.g. /update?freq=500000&duty=45)
  server.on("/update", []() {
    if (server.hasArg("freq") && server.hasArg("duty")) {
      currentFreq = server.arg("freq").toInt();
      currentDuty = server.arg("duty").toInt();
      
      // Safety clamps
      if(currentFreq < 1) currentFreq = 1;
      if(currentFreq > 1000000) currentFreq = 1000000;
      if(currentDuty < 5) currentDuty = 5;
      if(currentDuty > 95) currentDuty = 95;

      setPWM(currentFreq, currentDuty);
      server.send(200, "text/plain", "OK");
    } else {
      server.send(400, "text/plain", "Missing parameters");
    }
  });

  server.begin();
  Serial.println("HTTP server started");
}

void loop() {
  // Listen for incoming web requests
  server.handleClient();
}