[TH] ESP-IDF Ep.9 : LEDC (PWM) Output

บทความนี้กล่าวถึงการใช้งาน GPIO ของ ESP32 เพื่อทำหน้าที่นำออกสัญญาณดิจิทัลแบบ PWM หรือ Pulse Width Modulation หรือ LEDC (LED Control) ซึ่งทำให้สามารถสร้างคลื่นความถี่ หรือปรับสัดส่วนของสถานะ 1 และ 0 ใน 1 ลูกคลื่น ด้วยเหตุนี้ในกรณีที่ไม่มีภาค DAC ผู้เขียนยังคงสามารถปรับค่าเฉลี่ยของแรงดันที่ขานั้นได้ตามที่ต้องการ และสามารถประยุกต์ใช้ในการควบคุมมอเตอร์แบบเซอโวได้อีกด้วย ดังนั้น ในบทความนี้จึงเป็นการเรียนรู้การใช้งาน PWM และประยุกต์เข้ากับการส่งคลื่นความถี่แทน DAC  (จากบทความที่แล้ว) และการหรี่หลอดแอลอีดี โดยใช้บอร์ดทดลองดังภาพที่ 1

LEDC / PWM Labs.
ภาพที่ 1 การต่อใช้งานประกอบตัวอย่างการใช้งาน LEDC

โครงสร้างของโครงงาน

โครงสร้างของโครงงานของ ESP-IDF เป็นดังภาพที่ 2 คือ ในไดเร็กทอรีหรือโฟลเดอร์ของโครงงานจะมีไฟล์ CMakeList.txt และ sdkconfig กับไดเร็กทอรีชื่อ main สำหรับเก็บรหัสต้นฉบับของโครงงาน โดยในไดเร็กทอรีดังกล่าวมีไฟล์ภาษา C และ CMakeLists.txt

LEDC / PWM Files structure
ภาพที่ 2 โครงสร้างของโครงงาน

จากโครงสร้างในภาพที่ 2 ต้องสร้างโค้ดของไฟล์ CMakeLists.txt ดังนี้ ซึ่งเนื้อหาในโค้ดได้กำหนดรุ่นขั้นต่ำของโปรแกรม cmake และกำหนดค่าการใช้งานของ cmake เบื้องต้นตามจ้นฉบับที่มากับ ESP-IDF พร้อมทั้งตั้งชื่อโครงงานเป็น ep09

cmake_minimum_required(VERSION 3.5)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(ep09)

สิ่งที่เขียนในไฟล์ main/CMakeLists.txt เป็นดังต่อไปนี้ เพื่อกำหนดรายการไฟล์ที่จะต้องคอมไพล์ ซึ่งกำหนดไว้เป็น main.c และกำหนดไดเร็กทอรีที่เก็บไฟล์ส่วนหัวเอาไว้เป็นค่าว่างซึ่งหมายถึงที่เดียวกับ main.c หรือในไดเร็กทอรี main

idf_component_register(SRCS "main.c"
                    INCLUDE_DIRS "")

เมื่อสร้างโครงสร้างได้เหมือนดังภาพที่ 2 ให้สั่งเลือก target ของระบบเป็น ESP32 ดังนี้

idf.py set-target esp32

ส่วน sdkconfig เกิดจากการเรียกใช้คำสั่งต่อไปนี้ idf.py menuconfig

idf.py menuconfig

จากหน้าจอกำหนดการตั้งค่าให้เข้าไปที่ Component Config –> FreeRTOS และกำหนด Tick rate (Hz) เป็น 1000 ดังภาพที่ 3 หลังจากนั้นบันทึกและออกจากการตั้งค่า

FreeOS Tick Rate settings
ภาพที่ 3 ตั้งค่า Tick rate (Hz)

ที่มักจะลืมกันคือตั้งค่าขนาดความจุของหน่วยความจำรอม (Flash size) ดังภาพที่ 4 ให้ตรงกับขนาดที่ติดตั้งบนบอร์ด จากเสนู Serial flasher config ซึ่งในบทความใช้เป็น 4MB หลังจากนั้นกด S และ Q เพื่อบันทึกและออกจากการตั้งค่า

Serial Flasher size settings.
ภาพที่ 4 กำหนดขนาด flash size เป็น 4MB

LEDC

espressif ใช้คลาส LEDC เป็นโมดูลควบคุมความเข้มของหลอดแอลอีดีเป็นหลัก โดยโมดูลนี้ใช้ PWM เป็นตัวขับแรงดันที่ต่อกับหลอดแอลอีดี และสามารถใช้ได้ 16 ช่องสัญญาณที่สร้างลูกคลื่นได้อิสระจากกัน โดยแบ่งเป็น 2 ประเภท ดังภาพที่ 5 คือ

  • กลุ่มที่ทำงานแบบ High Speed Channel
  • กลุ่มที่ทำงานแบบ Low Speed Channel
LEDC / PWM Architecture
ภาพที่ 5 สถาปัตยกรรมของ PWM หรือ LEDC
ที่มา Figure 14-1 LED_PWM Architecture

การใช้งาน PWM หรือ LEDC ประกอบด้วย 3 ขั้นตอนดังนี้ (ดังภาพที่ 6)

  1. ตั้งค่าตัวตั้งเวลา
  2. ตั้งค่าช่องสัญญาณ
  3. เปลี่ยนสัญญาณของ PWM
LED PWM Controller's API
ภาพที่ 6 LED PWM Controller
ที่มา https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/ledc.html

การตั้งค่าตัวตั้งเวลา

เป็นการกำหนดค่าให้กำหนดโครงสร้างข้อมูลประเภท ledc_timer_config_t ซึ่งมีรายการสมาชิกในโครงสร้างดังนี้

struct ledc_timer_config_t {

ledc_mode_t speed_mode;

ledc_timer_bit_t duty_resolution;

ledc_timer_bit_t bit_num;

ledc_timer_t timer_num;

uint32_t freq_hz;

ledc_clk_cfg_t clk_cfg;

};

จากสมาชิกภายในโครงสร้างจะได้ว่า speed_mode เป็นการเลือกโหมดของการทำงานตามตัวเลือกต่อไปนี้

  • LEDC_HIGH_SPEED_MODE
  • LEDC_LOW_SPEED_MODE
  • LEDC_SPEED_MODE_MAX

ค่าของ duty_resolution เป็นการกำหนดความละเอียดของค่าดิวตี้ ส่วนค่า bit_num ได้ถูกเลิกใช้ตั้งเต่ ESP-IDF รุ่น 2.x แต่คงไว้เพื่อความเข้ากันได้เท่านั้น จึงใส่เป็น 0

timer_num เป็นหมายเลขตัวตั้งเวลาที่ต้องการใช้งาน ซึ่งกำหนดได้เป็น 0-3

  • LEDC_TIMER_0
  • LEDC_TIMER_1
  • LEDC_TIMER_2
  • LEDC_TIMER_3

ส่วนค่า freq_hz เป็นค่าความถี่ที่ต้องการสร้าง และค่าของ clk_cfg เป็นการกำหนดแหล่ง

  • LEDC_AUTO_CLK
  • LEDC_USE_REF_TICK ใช้แหล่งให้สัญญาณนาฬิกาจาก REF_TICK
  • LEDC_USE_ABP_TICK ใช้แหล่งให้สัญญาณนาฬิกาจาก ABP
  • LEDC_USE_RTC8M_CLK ใช้แหล่งให้สัญญาณนาฬิกาจาก RTC8M

คำสั่งของการตั้งค่าตัวตั้งเวลาเป็นดังนี้

esp_err_t ledc_timer_config( const ledc_timer_config_t * timer_conf)

ค่าคืนกลับจากคำสั่งเป็นดังนี้

  • ESP_OK ทำงานสำเร็จ
  • ESP_ERR_INVALID_ARG ค่าที่กำหนดมีความผิดพลาด
  • ESP_FAIL เกิดความผิดพลาดเนื่องจากการตั้งค่าเกี่ยวกับ pre-divider ที่นำไปใช้ในการคำนวฯค่าความถี่ และค่าเกี่ยวกับค่าของ duty_resolution

การตั้งค่าช่องสัญญาณ

ขั้นตอนที่ 2 เป็นการกำหนดค่าเกี่ยวกับช่องสัญญาณที่ใช้สำหรับนำออกสัญญาณไปขับวงจรภายนอก โดยใช้คำสั่ง ledc_channel_config() ตามรูปแบบต่อไปนี้

esp_err_t ledc_channel_config( const ledc_channel_config_t * ledc_conf )

จากรูปแบบของคำสั่งจะพบว่าอาร์กิวเมนต์หรือพารามิเตอร์ที่ต้องส่งให้กับคำสั่งนั้นเป็นข้อมูลในแบบ ledc_channel_config_t ซึ่งมีโครงสร้างดังนี้

struct ledc_channel_config_t {

int gpio_num;

ledc_mode_t speed_mode;

ledc_channel_t channel;

ledc_intr_type_t intr_type;

ledc_timer_t timer_sel;

uint32_t duty;

int hpoint;

unsigned int output_invert;

struct ledc_channel_config::[anonymous]flags;

};

จากโครงสร้างมีรายละเอียดดังนี้

  • gpio_num เป็นหมายเลขขาสำหรับใช้นำออกสัญญาณ
  • speed_mode โหมดทำงาน
    • LEDC_HIGH_SPEED_MODE
    • LEDC_LOW_SPEED_MODE
  • channel หมายเลขช่องสัญญาณ
    • LEDC_CHANNEL_0
    • LEDC_CHANNEL_1
    • LEDC_CHANNEL_2
    • LEDC_CHANNEL_3
    • LEDC_CHANNEL_4
    • LEDC_CHANNEL_5
    • LEDC_CHANNEL_6
    • LEDC_CHANNEL_7
    • LEDC_CHANNEL_MAX
  • intr_type เปิดหรือปิดการใช้การขัดจังหวะ
    • LEDC_INTR_DISABLE
    • LEDC_INTR_FADE_END
  • timer_sel แหล่งกำเนิดสัญญาณนาฬิกา มีค่า 0 ถึง 3
    • LEDC_TIMER_0
    • LEDC_TIMER_1
    • LEDC_TIMER_2
    • LEDC_TIMER_3
  • duty ค่าดิวตี้ ซึ่งมีค่าอยู่ในช่วง [0, 2duty_resolution)
  • hpoint ค่า hpoint สามารถกำหนดได้สูงสุด 0xfffff
  • output_invert
    • 0 ปิดการกลับค่าของผลลัพธ์ (output)
    • 1 เปิดการกลับบิตของผลลัพธ์
  • flags สถานะของแอลอีดี

ส่วนค่าคืนกลับจากคำสั่งได้แก่

  • ESP_OK
  • ESP_ERR_INVALID_ARG

เปลี่ยนสัญญาณของ PWM

มาถึงในขั้นตอนนี้ช่องสัญญาณที่กำหนดและสัญญาณ PWM จะเเริ่มถูกสร้างตามค่า duty และ freq ที่ระบุไว้ การเปลี่ยนแปลงค่าของ PWM สามารถกระทำได้ด้วยคำสั่ง 2 กลุ่มตามประเภทของการเลือกโหมดทำงาน

การเปลี่ยนเมื่อใช้โหมดแบบซอฟต์แวร์

การอ่านค่าดิวตี้ใช้คำสั่ง ledc_get_duty() ตามรูปแบบต่อไปนี้

ต่าดิวตี้=ledc_get_duty( ledc_mode_t speed_mode, ledc_channel_t channel)

คำสั่งตั้งค่าดิวตี้ใหม่กระทำได้ด้วยคำสั่งต่อไปนี้

esp_err_t ledc_set_duty( ledc_mode_t speed_mode, ledc_channel_t channel, ค่าดิวตี้)

และคำสั่งสำหรับสั่งให้ปรับเปลี่ยนค่าของดิวตี้ที่กำหนดใน ledc_set_duty() มีรูปแบบต่อไปนี้

esp_err_t ledc_update_duty( ledc_mode_t speed_mode,ledc_channel_t channel)

การเปลี่ยนเมื่อใช้โหมดของฮาร์ดแวร์

การใช้โหมดแบบฮาร์ดแวร์จะมีหลักการทำงานที่แตกต่างจากแบบซอฟต์แวร์ คือ สามารถทำการเฟด (fade) ค่าดิวตี้จากค่าหนึ่งไปยังอีกค่าหนึ่งได้ ซี่งถ้าต้องการเปิดการทำงานจะต้องเรียกใช้คำสั่ง ledc_fade_function_install() ดังรูปแบบต่อไปนี้

esp_err_t ledc_fade_func_install( int intr_alloc_flags )

เมื่อเลิกใช้งานการทำงานโหมดของฮาร์ดแวร์จะต้องเรียกคำสั่งดังนี้

ledc_fade_func_uninstall()

การทำงานในด้านของค่าดิวตี้นั้นจะใช้หลักการค่อยเปลี่ยนจากค่าหนึ่งไปยังอีกค่าหนึ่ง ซึ่งคำสั่งที่เกี่ยวข้องสำหรับการตั้งค่าการเฟดมีดังนี้

  1. ledc_fade_start( ledc_mode_t speed_mode, ledc_channel_t chennel, ledc_fade_mode_t fade_mode ) สำหรับเริ่มต้นทำงาน
  2. ledc_set_fade_with_time( ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t target_duty, int max_fade_time_ms) สำหรับตั้งค่าระยะเวลาที่ใช้ในการเปลี่ยนค่าดิวตี้จากที่เป็นอยู่มาเป็น target_duty ในเวลา max_fade_time_ms มิลลิวินาที
  3. ledc_set_fade_with_step( ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t target_duty, uint32_t scale, uint32_t cycle_num) กำหนดให้ทำการเฟดโดยกำหนดจำนวนขั้นของการเฟด cycle_num ครั้ง โดยแต่ละครั้งเปลี่ยนครั้งละ scale
  4. ledc_set_fade( ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t duty, ledc_duty_direction_t fade_direction, uint32_t step_num, uint32_t duty_cycle_num, uint32_t duty_scale)

ค่าของ ledc_fade_mode_t ได้แก่

  • LEDC_FADE_NO_WAIT เปลี่ยนโดยทันทีทันใด
  • LEDC_FADE_WAIT_DONE ค่อย ๆ เปลี่ยนค่าไปเป็นค่าดิวตี้ใหม่

ค่าของ ledc_duty_direction_t ได้แก่

  • LEDC_DUTY_DIR_DECREASE  ค่อย ๆ ลดค่าลง
  • LEDC_DUTY_DIR_INCREASE ค่อย ๆ เพิ่มค่า

การกำหนดขาและหยุดทำงาน

การกำหนดขาสำหรับนำออกสัญญาณใช้คำสั่งดังนี้

esp_err_t ledc_set_pin(int gpio_num, ledc_mode_t speed_mode, ledc_channel_t ledc_channel )

ส่วนการปิดการสร้างสัญญาณนาฬิกาทำได้ด้วยการเรียกคำสั่ง ledc_stop() ตามรูปแบบการใช้งานดังต่อไปนี้

esp_err_t ledc_stop(ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t idle_level)

โดยค่า idle_level คือค่าระดับสัญญาณดิจิทัลหลังจากที่ PWM หยุดทำงานแล้ว

การใช้งาน PWM หรือ LEDC จะต้องนำเข้าไฟล์ส่วนหัวดังนี้ เพื่อใช้สำหรับเปิดหรือปิดการทำงานดังนี้

#include <driver/ledc.h>

กรณีที่ต้องการใช้งาน GPIO แบบอื่น ๆ สามารถเข้าไปอ่านบทความต่าง ๆ ดังต่อไปนี้เพิ่มเติม

  1. นำออกข้อมูลสัญญาณดิจิทัล
  2. นำเข้าสัญญาณดิจิทัล
  3. นำเข้าสัญญาณแอนาล็อก
  4. นำออกสัญญาณแอนาล็อก (ตอน 2)

ตัวอย่างโปรแกรม

ตัวอย่างโปรแกรมที่ esp-idf เตรียมไว้ให้มี 2 ตัวคือ

อุปกรณ์

อุปกรณ์ที่ใช้ในการทดลองได้แก่

  1. บอร์ด esp32
  2. บอร์ดทดลอง
  3. หลอดแอลอีดี และตัวต้านทาน
  4. โมดูลลำโพง
    1. ทรานซิสเตอร์
    2. ตัวต้านทาน 2 ตัว
    3. บัซเซอร์

โค้ดโปรแกรม

ตัวอย่างโปรแกรมสร้างคลื่นความถี่ 262Hz ซึ่งเป็นค่าความถี่ของเสียงตัว C (เสียง “โด”) ของเปียโนเป็นเวลา 2 วินาที หลังจากนั้นทำการเร่งและหรี่แสงของหลอดแอลอีดี จากดับเป็นสว่างมากสุดและจากสว่างมากสุดกลับเป็นดับ ซึ่งโค้ดทั้งหมดเขียนได้ดังนี้

#include <stdio.h>
#include <time.h>
#include <string.h>
#include <math.h>
#include <sdkconfig.h>
#include <driver/gpio.h>
#include <driver/ledc.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>

#define pinSpk 26 
#define pinLED 23 

#define LEDC_DUTY_MAX   (8191)
#define LEDC_DUTY       (4095) // ปรับค่าดิวตี้เป็น 50% จาก (213-1)*50/100 จึงได้ค่าเป็น 4095

void testSpk() {
  // Speaker
  printf("Speaker ... C .. ");
  ledc_timer_config_t ledc_timer = {
        .speed_mode       = LEDC_LOW_SPEED_MODE,
        .timer_num        = LEDC_TIMER_0, 
        .duty_resolution  = LEDC_TIMER_13_BIT, // 213
        .freq_hz          = 262,
        .clk_cfg          = LEDC_AUTO_CLK
  };
  ledc_timer_config(&ledc_timer);

  ledc_channel_config_t ledc_channel = {
        .speed_mode     = LEDC_LOW_SPEED_MODE,
        .channel        = LEDC_CHANNEL_0,
        .timer_sel      = LEDC_TIMER_0,
        .intr_type      = LEDC_INTR_DISABLE,
        .gpio_num       = pinSpk,
        .duty           = 0, // Set duty to 0%
        .hpoint         = 0
  };
  ledc_channel_config(&ledc_channel);

  // สร้างความถี่ 262Hz
  ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 262);
  ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);

 // หน่วงเวลา 2 วินาที
  vTaskDelay( 2000/portTICK_PERIOD_MS ); 

  printf("done.\n");

  // ปิดการทำงานของ LEDC หรือ PWM
  ledc_stop(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0);
}

void testLED() {
  // fade the LED
  printf("LED ... ");
  ledc_timer_config_t ledc_timer = {
        .speed_mode       = LEDC_LOW_SPEED_MODE,
        .timer_num        = LEDC_TIMER_1,
        .duty_resolution  = LEDC_TIMER_13_BIT,
        .freq_hz          = 50,  // Set output frequency at 5 kHz
        .clk_cfg          = LEDC_AUTO_CLK
  };
  ledc_timer_config(&ledc_timer);
  ledc_channel_config_t ledc_channel = {
        .speed_mode     = LEDC_LOW_SPEED_MODE,
        .channel        = LEDC_CHANNEL_1,
        .timer_sel      = LEDC_TIMER_1,
        .intr_type      = LEDC_INTR_DISABLE,
        .gpio_num       = pinLED,
        .duty           = 0, // Set duty to 100%
        .hpoint         = 0
  };
  ledc_channel_config(&ledc_channel);

  int i;
  // ปรับค่าดิวตี้เพื่อให้หลอดเปลี่ยนจากดับเป็นสว่าง
  for (i=200; i>0; i--) {
    ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_1,i*40);
    ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_1);
    // หน่วงเวลาเพื่อให้เห็นผลของการเปลี่ยน
    vTaskDelay( 50/portTICK_PERIOD_MS ); 
  }

  // เปล่ยนค่าดิวตี้เพื่อเปลี่ยนจากสว่างเป็นดับ
  for (i=0; i<200; i++) {
    ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_1,i*40);
    ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_1);
    // หน่วงเวลาเพื่อให้เห็นผลของการเปลี่ยน
    vTaskDelay( 50/portTICK_PERIOD_MS ); 
  }

  printf("done.\n");

  // ปิดการทำงานของ LEDC หรือ PWM
  ledc_stop(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_1, 1);
}

void app_main(void)
{
  printf("Ep.09 DAC\n"); 
  testSpk();
  testLED();
  printf("End of program\n");
}

คอมไพล์และอัพโหลด

ทำการคอมไพล์ หลังจากนั้น flash ลงชิพ และเข้าโปรแกรม Serial Monitor สั่งงานดังนี้

idf.py -p /dev/ttyUSB0 build flash monitor

ตัวอย่างผลลัพธ์ของโปรแกรมเป็นดังภาพที่ 7

LEDC / PWM demo result.
ภาพที่ 7 ผลลัพธ์จากโปรแกรม ep9

สรุป

จากบทความนี้จะพบว่า การการใช้ PWM หรือ LEDC นั้นประกอบด้วย 3 ขั้นตอน คือ

  1. ตั้งค่าตัวตั้งเวลา ด้วย ledc_timer_config()
  2. ตั้งค่าช่องสัญญาณ ledc_channel_config()
  3. สั่งงาน ledc_set_duty(), ledc_update_duty() และ ledc_stop()

แต่อย่างไรก็ดี ตัวอย่างนี้ครั้งนี้เป็นเฉพาะเรื่องการใช้ PWM แบบไม่ได้ใช้ฮาร์ดแวร์ในส่วนของ Timer เพื่อทำการเฟดค่า แต่ในตัวอย่างหลอดแอลอีดีนั้นได้ทำการเฟดค่าด้วยการเขียนโปรแกรมเพื่อวนรอบ แต่อย่างไรก็ดี เมื่อทางทีมงานเราได้เขียนบทความการขัดจังหวะ และการใช้งานตัวตั้งเวลาเป็นที่เรียบร้อยแล้วจะกลับมาทำตัวอย่างสำหรับเรื่องนี้อีกครั้ง และสุดท้ายขอให้สนุกกับการเขียนโปรแกรมครับ

ท่านใดต้องการพูดคุยสามารถคอมเมนท์ได้เลยครับ

แหล่งอ้างอิง

  1. WiKiPedia : Pulse-width modulation
  2. ESP-IDF : LEDC

(C) 2020-2021, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-10-13, 2021-12-27