ESP32 (EP.7) ESP32 ทำเป็น RC meter

การใช้ ESP32 ทำเป็น RC meter …. ขอเริ่มที่การวัดค่าความต้านทานก่อน  

พื้นฐานที่ง่ายที่สุดและได้ผลดีมากพอสมควรก็คือการต่อตัวความต้านทานเป็นวงจรแบ่งแรงดัน (Voltage divider)  วงจรแบ่งแรงดันนั้นรายละเอียดของวงจรและสมการที่ใช้เป็นไปตามภาพด้านล่างนี้  

สมการที่ใช้จะไม่ยากเลยเนื่องจากมันก็เป็นกฎของโอห์มธรรมดานี่เองครับ  การประยุกต์ใช้วงจรแบ่งแรงดันเพื่อทำให้วัดค่าความต้านทานได้ก็คือ  ต่อ Vout ของวงจรแบ่งแรงดันกับ GPIO เพื่อวัดค่าแรงดัน  โดยมีแรงดัน Vin นำมาจากไฟ 3.3 โวลต์ของ ESP32 เอง  และ Vout ก็ต่อตรงกับ GPIO ของ ESP32 ใน ADC mode

  ในสมการล่างนี้  ตัวความต้านทานที่เราต้องการจะวัดค่าก็คือตัว R….. ส่วนตัว R2 จะใช้ R ค่าคงที่  ซึ่งจากค่าแรงดัน input อ้างอิง 3.3 โวลต์ค่าของ R2 ที่เหมาะสมก็คือ 22kΩ …. ดังนั้นการหาสมการสุดท้ายที่จะใส่ใน code เราก็จะต้องย้ายข้างสมการ โดยในสมการให้แทนค่าแรงดัน Vin = 3.3 โวลต์และค่าเท่ากับ 22kΩ  สุดท้ายแล้วจะได้สมการที่จะนำไปใส่ใน code ตามภาพด้านล่างนี้


ส่วนการวัดค่า Capacitor วิธีที่ง่ายที่สุดก็คือจะต้องต่อวงจรเพิ่มโดยใช้ IC 555 timer ครับ
จะขออธิบายถึงวงจรสำหรับ IC 555 timer ตัวนี้ก่อน (ภาพล่าง) การต่อ IC 555 ในลักษณะนี้จะเรียกว่า Astable multivibrator  สูตรเพื่อหาความถี่จะมาจากค่าความสัมพันธ์ระหว่างอุปกรณ์ 3 ตัวก็คือ R1 , R2 และ C  ซึ่งสูตรก็ง่ายมากรายละเอียดตามภาพล่าง

concept การทำงานก็คือ  เมื่อเราจิ้ม capacito rที่ต้องการวัดลงไป  ตัว IC 555 ก็จะกำเนิดความถี่ป้อนเข้า GPIO (digital mode) ของ ESP32 และวัดความถี่ + แปลงไปเป็นค่า Capacitor ที่เราจะวัด  ในการ code ให้วัดค่า capacitor วิธีการที่ดีที่สุดก็คือใช้คุณสมบัติอันแม่นยำอย่างหนึ่งของ MCPWM โดยมันจะวัดคาบเวลาระหว่างของขาขึ้น (Rising edge) ของคลื่นความถี่ที่กำเนิดจาก IC 555 เข้ามาและคำนวณออกมาเป็นค่าของ capacitor เป็นไปตามสมการด้านล่างนี้


อุปกรณ์ทั้งหมดมีเพียงเท่านี้
จากภาพนี้ ในวงจรส่วนบนก็คือวงจรกำเนิดความถี่ของ IC 555 และสังเกตในจอภาพก็จะแสดงค่า Resistor , Capacitor

ในภาพล่างนี้ เอา capacitor ขนานกัน 2 ตัว (100 µF + 10 µF) ก็จะอ่านได้ค่า capacitance ที่ถูกต้องก็คือ 111.5 µF …. และใช้สาย jumper จิ้มวัด R 5.6kΩ ในจอภาพก็แสดงค่า 5.59 kΩ

Code ทั้งหมด

#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include "driver/mcpwm_cap.h"

// Hardware Pin Definitions
#define CAP_INPUT_PIN   11   // Capacitance input (MCPWM)
#define RES_ANALOG_PIN  1    // Resistance input (ADC1_CH0 / GPIO 1)
#define I2C_SDA         40   // Shared OLED SDA
#define I2C_SCL         42   // Shared OLED SCL
#define SCREEN_ADDRESS  0x3C

// OLED Display Setup
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SH1106G display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

// --- Capacitance Meter Constants & Variables ---
const double TIMER_CONSTANT = 1.44 / 3000.0; // 555 Timer Astable Mode (R1=1000, R2=1000)
const uint32_t APB_CLOCK = 80000000;         // 80 MHz MCPWM clock source

volatile uint32_t last_capture_ticks = 0;
volatile uint32_t current_prescaler = 1;
volatile bool new_data_available = false;
volatile uint32_t last_interrupt_time = 0;

mcpwm_cap_channel_handle_t cap_chan = NULL;
mcpwm_cap_timer_handle_t cap_timer = NULL;

// Global variables to store latest measurements
double global_frequency = 0.0;
bool cap_connected = false;

// --- Resistance Meter Constants & Variables ---
unsigned long last_resistor_measure = 0;
const unsigned long resistor_interval = 500; // Measure resistance every 500ms
float global_resistance = -1.0;              // -1.0 indicates "OPEN" or disconnected

// Forward Declarations
String formatWithDigits(long value);
void set_capture_prescaler(uint32_t prescale_val);

// --- Capacitance MCPWM ISR Callback ---
static bool IRAM_ATTR on_capture_callback(mcpwm_cap_channel_handle_t cap_chan, const mcpwm_capture_event_data_t *edata, void *user_data) {
    static uint32_t last_trigger_count = 0;
    static uint32_t pulse_counter = 0;
   
    uint32_t current_count = edata->cap_value;
    pulse_counter++;
   
    if (pulse_counter >= current_prescaler) {
        last_capture_ticks = current_count - last_trigger_count;
        last_trigger_count = current_count;
        pulse_counter = 0;
        
        last_interrupt_time = millis();
        new_data_available = true;
    }
    return false;
}

void set_capture_prescaler(uint32_t prescale_val) {
    current_prescaler = prescale_val;
}

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

    // 1. Initialize shared I2C bus for the OLED display
    Wire.begin(I2C_SDA, I2C_SCL);
    if(!display.begin(SCREEN_ADDRESS, true)) {
        Serial.println(F("SH1106 allocation failed"));
        for(;;);
    }
   
    display.clearDisplay();
    display.setTextColor(SH110X_WHITE);
    display.setTextSize(1);
    display.setCursor(0, 0);
    display.println("Initializing Combo...");
    display.display();

    // 2. Configure Resistance ADC Input
    analogReadResolution(12); // 0-4095
    analogSetPinAttenuation(RES_ANALOG_PIN, ADC_11db); // ~0V to 3.1V - 3.3V

    // 3. Initialize Capacitance MCPWM Capture Timer
    mcpwm_capture_timer_config_t timer_config = {};
    timer_config.group_id = 0;
    timer_config.clk_src = MCPWM_CAPTURE_CLK_SRC_DEFAULT;
    ESP_ERROR_CHECK(mcpwm_new_capture_timer(&timer_config, &cap_timer));

    // 4. Initialize Capacitance MCPWM Capture Channel
    mcpwm_capture_channel_config_t cap_chan_config = {};
    cap_chan_config.gpio_num = CAP_INPUT_PIN;
    cap_chan_config.prescale = 1;
    cap_chan_config.flags.neg_edge = 0;
    cap_chan_config.flags.pos_edge = 1;
    cap_chan_config.flags.pull_up = 0;
    cap_chan_config.flags.pull_down = 0;
    ESP_ERROR_CHECK(mcpwm_new_capture_channel(cap_timer, &cap_chan_config, &cap_chan));

    // 5. Register Capacitance ISR Callback
    mcpwm_capture_event_callbacks_t cbs = {};
    cbs.on_cap = on_capture_callback;
    ESP_ERROR_CHECK(mcpwm_capture_channel_register_event_callbacks(cap_chan, &cbs, NULL));

    // 6. Enable and Start Capacitance Hardware Peripherals
    ESP_ERROR_CHECK(mcpwm_capture_channel_enable(cap_chan));
    ESP_ERROR_CHECK(mcpwm_capture_timer_enable(cap_timer));
    ESP_ERROR_CHECK(mcpwm_capture_timer_start(cap_timer));

    last_interrupt_time = millis();
}

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

    // ==========================================
    // 1. RESISTANCE MEASUREMENT LOGIC (Every 500ms)
    // ==========================================
    if (currentMillis - last_resistor_measure >= resistor_interval) {
        last_resistor_measure = currentMillis;
        
        int rawADC = analogRead(RES_ANALOG_PIN);
        if (rawADC <= 10) {
            global_resistance = -1.0; // Mark as OPEN circuit
        } else {
            float vGpio1 = (rawADC * 3.3f) / 4095.0f;
            float resistance = (73480.0f / vGpio1) - 22000.0f;
            if (resistance < 0.0f) resistance = 0.0f;
            global_resistance = resistance;
        }
    }

    // ==========================================
    // 2. CAPACITANCE MEASUREMENT LOGIC (Asynchronous)
    // ==========================================
    // Check if pulse stream stopped (Timeout after 2.5 seconds)
    if (millis() - last_interrupt_time > 2500) {
        cap_connected = false;
        global_frequency = 0.0;
        set_capture_prescaler(1);
    }
    // New pulse batch caught by ISR
    else if (new_data_available) {
        new_data_available = false;

        noInterrupts();
        uint32_t ticks = last_capture_ticks;
        uint32_t active_prescaler = current_prescaler;
        interrupts();

        if (ticks > 0) {
            global_frequency = ((double)APB_CLOCK * (double)active_prescaler) / (double)ticks;
            
            if (global_frequency > 600000.0) {
                cap_connected = false;
            } else {
                cap_connected = true;
            }

            // Auto-Ranging Software Engine
            if (ticks < 250 && active_prescaler == 1) {
                set_capture_prescaler(16);
            } else if (ticks > 50000 && active_prescaler == 16) {
                set_capture_prescaler(1);
            }
        }
    }

    // ==========================================
    // 3. UNIFIED OLED DISPLAY UPDATE (Every 300ms)
    // ==========================================
    static uint32_t last_display_update = 0;
    if (millis() - last_display_update > 300) {
        last_display_update = millis();
        
        display.clearDisplay();
        display.setTextColor(SH110X_WHITE);

        // --- TOP HALF: RESISTANCE METER ---
        display.setTextSize(1);
        display.setCursor(0, 0);
        display.print("RESISTANCE:");
        
        display.setCursor(0, 10);
        display.setTextSize(2);
        if (global_resistance < 0.0f) {
            display.print("OPEN");
        } else if (global_resistance < 1000.0f) {
            display.print(formatWithDigits((long)global_resistance));
            display.print(" ");
            display.print(" Ohm"); // If you just want it to say "Ohm" cleanly
        } else {
            float kOhm = global_resistance / 1000.0f;
            display.print(kOhm, 2);
            display.print(" K");
            display.print("Ohm"); // If you just want it to say "Ohm" cleanly
        }

        // Dividers visually separates the two regions
        display.drawLine(0, 30, 128, 30, SH110X_WHITE);

        // --- BOTTOM HALF: CAPACITANCE METER ---
        display.setTextSize(1);
        display.setCursor(0, 34);
        display.print("CAPACITANCE:");
        
        if (!cap_connected) {
            display.setCursor(0, 46);
            display.setTextSize(2);
            display.print("OPEN");
        } else {
            double capacitance_F = TIMER_CONSTANT / global_frequency;
            double capacitance_uF = capacitance_F * 1000000.0;

            display.setCursor(0, 46);
            display.setTextSize(2);
            if (capacitance_uF < 0.1) {
                display.print(capacitance_uF, 4);
            } else if (capacitance_uF < 10.0) {
                display.print(capacitance_uF, 3);
            } else {
                display.print(capacitance_uF, 1);
            }
            display.setTextSize(1);
            display.print(" uF");
        }

        display.display();
    }
}

// Helper function to handle digit grouping (e.g., 22,000 instead of 22000)
String formatWithDigits(long value) {
    String valStr = String(value);
    String result = "";
    int len = valStr.length();
   
    for (int i = 0; i < len; i++) {
        if (i > 0 && (len - i) % 3 == 0) {
            result += ",";
        }
        result += valStr[i];
    }
    return result;
}

ESP32 (EP.6) ESP32 กับการรับส่งสัญญาณ Infrared

 ESP32 (EP.6) วันนี้จะขอเสนอการใช้ ESP32 กับตัวรับและส่งสัญญาณ Infrared  นี่เป็นการใช้งานอีกประเภทหนึ่งที่มีประโยชน์มากโดยใช้กับตัวรับสัญญาณอินฟราเรด 38 – 40 KHz จาก remote เครื่องใช้ไฟฟ้าต่าง ๆ และแอร์  และใช้ ESP32 + Infrared LED เพื่อส่งสัญญาณแทนการใช้ remote control ไปควบคุมอุปกรณ์อะไรก็ได้ในบ้าน

กระทู้เก่าทั้งหมด
🛑 EP.1  แนะนำเบื้องต้นให้รู้จัก ESP32
https://pantip.com/topic/44159372
🛑 EP.2  ใช้งาน ESP32 กับ web dashboard
https://pantip.com/topic/44161526
🛑 EP.3  ใช้งาน ESP32 กับ sensor ต่าง ๆ
https://pantip.com/topic/44163637
🛑 EP.4  ESP32 กับ dashboard + แจ้งเตือนผ่าน Line
https://pantip.com/topic/44165778
🛑 EP.5  ESP32 กับงาน PWM
https://pantip.com/topic/44167818


concept ในการใช้งานก็มีเพียง 2 อย่างเท่านั้นครับ
อย่างแรกก็คือเขียน Code ให้ ESP32 รับสัญญาณ infrared จากรีโมทของเครื่องใช้ไฟฟ้า / แอร์ก่อนเพื่อจะได้รู้ค่า data ของรีโมทปุ่มต่าง ๆ  data และมันจะแสดงออกมาเป็นชุดข้อมูลเรียกว่า Protocol  ซึ่ง ESP32 จะมี library อยู่ตัวหนึ่งคือ IRremoteESP8266  library ตัวนี้จะรวบรวมเอา protocol ของรีโมทเครื่องใช้ไฟฟ้ากว่า 100 แบรนด์เกือบ 500 รุ่นเข้าไว้ใน library ตัวนี้  และเมื่อเราทดลองยิงสัญญาณจากรีโมทเครื่องใช้ไฟฟ้า/แอร์ที่บ้านเรา หากเครื่องนั้นหรือแอร์รุ่นนั้นอยู่ใน library ตัวนี้มันก็จะแสดงค่า protocol ออกมาเป็นชุด Hexadecimal (HEX) data

ขอเริ่มจากการใช้ตัวรับสัญญาณรีโมทความถี่ 38 KHz พร้อมกับการเขียน code สั้น ๆ …. อุปกรณ์ที่ใช้ตามภาพด้านล่างนี้เลยครับ

Code ทั้งหมดของการรับ protocol รีโมท

#include <Arduino.h>
#include <IRremoteESP8266.h>
#include <IRrecv.h>
#include <IRutils.h>

// Define the GPIO pin connected to the TSOP3848 OUT pin
const uint16_t kRecvPin = 4; 

// 1024 bytes is usually enough for long AC data packets
const uint16_t kCaptureBufferSize = 1024; 

// High timeout ensures the entire long AC frame is captured (in milliseconds)
const uint8_t kTimeout = 50; 

// Initialize the IR receiver object
IRrecv irrecv(kRecvPin, kCaptureBufferSize, kTimeout, true);
decode_results results;

void setup() {
  Serial.begin(115200);
  while (!Serial) {
    delay(50); // Wait for Serial Monitor to connect (needed for ESP32-S3 USB)
  }
  
  Serial.println("\n--- ESP32-S3 AC IR Decoder Starting ---");
  irrecv.enableIRIn(); // Start the receiver
  Serial.print("Listening for IR signals on GPIO ");
  Serial.println(kRecvPin);
}
void loop() {
  if (irrecv.decode(&results)) {
    Serial.println("==========================================");
    
    // Check if the protocol is recognized
    if (results.decode_type != decode_type_t::UNKNOWN) {
      Serial.print("Protocol Recognized! Brand: ");
      Serial.println(typeToString(results.decode_type));
    } else {
      Serial.println("Protocol: UNKNOWN (Not explicitly supported by the library yet)");
    }

    // Display basic information
    Serial.print("Value/Hex: 0x");
    serialPrintUint64(results.value, 16);
    Serial.println();
    Serial.print("Bits: ");
    Serial.println(results.bits);
    
    // Output the raw data in C++ format
    Serial.println();
    Serial.println("Raw Timing Data (Use this if protocol is UNKNOWN):");
    yield(); // Prevent watchdog triggers
    
    // FIX: Using the correct, built-in library function here
    String sourceCode = resultToSourceCode(&results);
    Serial.println(sourceCode);
    
    Serial.println("==========================================\n");
    
    // Resume listening for the next signal
    irrecv.resume(); 
  }
  delay(100);
}

ผมทดลองใช้รีโมทของแอร์ Carrier ยิงเข้าไปผลลัพธ์ที่ได้ก็ตามภาพด้านล่าง  protocol ที่อ่านได้จะมี 2 ส่วนที่สำคัญ ….
1. ส่วนแรกเรียกว่า raw data ก็คือจะเป็นชุดของตัวเลข 3 – 4 หลักหลายสิบชุด แต่ละตัวเลขคือค่าของ pulse width (หน่วย μs) ของสัญญาณ infrared ที่รีโมทตัวนั้นส่งออกมาเมื่อกดปุ่มนั้น ๆ
2. ส่วนที่ 2 คือ protocol ที่ใช้ง่ายมากก็คือเป็นตัวเลขที่เข้ารหัสแล้วเพียงบรรทัดเดียวเรียกว่า Hexadecimal (HEX) ซึ่งในการใช้งานเราก็เอาชุด HEX protocol บรรทัดเดียวนี่แหละครับไปใช้ใน code เพื่อส่งไปที่เครื่องใช้ไฟฟ้า/แอร์

สัญญาณ infrared ที่ส่งออกจากรีโมทจะเป็นลักษณะที่เรียกว่า Pulse train  คือเป็นคลื่นสี่เหลี่ยมที่มีสารพัด pulse width ต่อเนื่องกันยาวมาก ซึ่งจะส่งสั้นเพียง 0.15 – 0.25 วินาทีเท่านั้น


อย่างที่ 2 คือนำ protocol ที่รับได้จากการกดปุ่มรีโมทไปใช้  ก็คือเราก็จะต้องเอา HEX protocol ของรีโมทที่เรากดในปุ่มที่เราต้องการไปใส่ใน code  อย่างเช่นกดเปิดแอร์ , กดอุณหภูมิ 26 องศา , กดพัดลมเบา …. ทุกปุ่มที่เรากดมันก็จะให้ตัว protocol ที่แตกต่างกัน  การใช้งานในลักษณะเช่นนี้ก็คือสั่งเปิดเครื่องใช้ไฟฟ้าใด ๆ จากนอกบ้าน อย่างเช่นเรากำลังเดินทางกลับบ้านอากาศร้อนมากอยากจะเปิดแอร์ก็เพียงแค่หยิบมือถือขึ้นมาแล้วสั่งเปิดแอร์จาก web dashboard ในมือถือของเรา  และตัว ESP32 ที่บ้านก็จะส่งสัญญาณ protocol จาก infrared LED ไปที่แอร์เพื่อสั่งให้แอร์เปิด

ใน project ที่จะสาธิตนี้อุปกรณ์ที่ใช้เป็นไปตามภาพด้านล่างนี้ครับ

นี่คือคลิปที่สาธิตให้ดูว่า ESP32 สามารถส่งสัญญาณ infrared protocol จาก infrared LED เพื่อควบคุมแอร์แทนการกดรีโมทจริง ๆ ได้ …. ในคลิปหลังจากไฟสีแดงกระพริบ 9 ครั้งแล้วให้สังเกตที่ infrared LED 2 ตัวจะเห็นว่ามีจุดสว่างจาง ๆ ขึ้นแว้บนึง นั่นก็คือมันส่งคลื่นแสงอินฟราเรดไปที่แอร์และเราจะได้ยินเสียงแอร์ทำงานดัง “ตี๊ด”  ซึ่งปกติเราดวงตาจะมองไม่เห็นแสง infrared ที่ส่งจาก infrared LED นี้ แต่กล้องมือถือสามารถเห็นได้แบบจาง ๆ

Code ทั้งหมด

#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <IRremoteESP8266.h>
#include <IRsend.h>
#include <ir_Neoclima.h>

// OLED Display Configuration
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET     -1 // Share reset pin or set to -1 if not used
Adafruit_SH1106G display = Adafruit_SH1106G(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Hardware Pins
const uint16_t kIrLedPin = 14; 
const uint16_t kIndicatorLedPin = 11; 
const int I2C_SDA_PIN = 40;
const int I2C_SCL_PIN = 42;

IRNeoclimaAc neoclimaAC(kIrLedPin);

// Intervals in milliseconds
const unsigned long INTERVAL_STATE_1_TO_2 = (3 * 60) * 1000UL; 
const unsigned long INTERVAL_STATE_2_TO_1 = (9 * 60) * 1000UL;       

bool isCurrentStateOne = true;
unsigned long previousMillis = 0;
unsigned long lastOledUpdateMillis = 0;

// Raw 12-byte states captured from the original remote control
uint8_t rawStateOne[12] = {0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x7A, 0x80, 0x2A, 0xA5, 0xCD};
uint8_t rawStateTwo[12] = {0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x7A, 0x80, 0x2D, 0xA5, 0xCF};

// Function declarations
void sendStateOne();
void sendStateTwo();
void flashLED(); 
void updateOLEDCountdown(unsigned long elapsed, unsigned long totalDuration, const char* stateName);

void setup() {
  pinMode(kIndicatorLedPin, OUTPUT); 
  
  // Initialize Custom I2C Pins for ESP32-S3
  Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
  
  // Initialize SH1106 OLED (Address 0x3C is common for these modules)
  if(!display.begin(0x3C, true)) { 
    // If initialization fails, loop infinitely or handle error
    while(true);
  }
  
  display.clearDisplay();
  display.setTextColor(SH110X_WHITE);
  display.display();

  neoclimaAC.begin();
  
  // Initial transmission
  sendStateOne();
  previousMillis = millis(); 
  updateOLEDCountdown(0, INTERVAL_STATE_1_TO_2, "State One");
}

void loop() {
  unsigned long currentMillis = millis();
  unsigned long currentInterval = isCurrentStateOne ? INTERVAL_STATE_1_TO_2 : INTERVAL_STATE_2_TO_1;
  unsigned long elapsedMillis = currentMillis - previousMillis;

  // Update the countdown display once every second
  if (currentMillis - lastOledUpdateMillis >= 1000UL) {
    lastOledUpdateMillis = currentMillis;
    
    // Prevent elapsed from exceeding total interval before state switch clears it
    if (elapsedMillis > currentInterval) elapsedMillis = currentInterval; 
    
    updateOLEDCountdown(elapsedMillis, currentInterval, isCurrentStateOne ? "State One" : "State Two");
  }

  // State switching logic
  if (isCurrentStateOne) {
    if (elapsedMillis >= INTERVAL_STATE_1_TO_2) {
      sendStateTwo();
      isCurrentStateOne = false;     
      previousMillis = currentMillis; 
      lastOledUpdateMillis = currentMillis;
      updateOLEDCountdown(0, INTERVAL_STATE_2_TO_1, "State Two");
    }
  } else {
    if (elapsedMillis >= INTERVAL_STATE_2_TO_1) {
      sendStateOne();
      isCurrentStateOne = true;      
      previousMillis = currentMillis; 
      lastOledUpdateMillis = currentMillis;
      updateOLEDCountdown(0, INTERVAL_STATE_1_TO_2, "State One");
    }
  }
}

void updateOLEDCountdown(unsigned long elapsed, unsigned long totalDuration, const char* stateName) {
  long remainingSeconds = (totalDuration - elapsed) / 1000UL;
  if (remainingSeconds < 0) remainingSeconds = 0;

  int minutes = remainingSeconds / 60;
  int seconds = remainingSeconds % 60;

  display.clearDisplay();
  
  // Header: Active State
  display.setTextSize(1);
  display.setCursor(0, 4);
  display.print("Sending: ");
  display.println(stateName);
  
  // Visual divider line
  display.drawFastHLine(0, 18, 128, SH110X_WHITE);

  // Large Countdown Display
  display.setTextSize(2);
  display.setCursor(12, 32);
  
  // Format matching: "3m 30s" or "9m 0s"
  display.print(minutes);
  display.print("m ");
  display.print(seconds);
  display.print("s");
  
  display.display();
}

void flashLED() {
  for (int i = 0; i < 9; i++) {
    digitalWrite(kIndicatorLedPin, HIGH);
    delay(166); 
    digitalWrite(kIndicatorLedPin, LOW);
    delay(167); 
  }
}

void sendStateOne() {
  flashLED(); 
  neoclimaAC.setRaw(rawStateOne);
  neoclimaAC.send();
}

void sendStateTwo() {
  flashLED(); 
  neoclimaAC.setRaw(rawStateTwo);
  neoclimaAC.send();
}

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