บทความนี้กล่าวถึงการใช้งาน GPIO ของ ESP32 เพื่อทำหน้าที่นำออกสัญญาณดิจิทัลแบบ PWM หรือ Pulse Width Modulation หรือ LEDC (LED Control) ซึ่งทำให้สามารถสร้างคลื่นความถี่ หรือปรับสัดส่วนของสถานะ 1 และ 0 ใน 1 ลูกคลื่น ด้วยเหตุนี้ในกรณีที่ไม่มีภาค DAC ผู้เขียนยังคงสามารถปรับค่าเฉลี่ยของแรงดันที่ขานั้นได้ตามที่ต้องการ และสามารถประยุกต์ใช้ในการควบคุมมอเตอร์แบบเซอโวได้อีกด้วย ดังนั้น ในบทความนี้จึงเป็นการเรียนรู้การใช้งาน PWM และประยุกต์เข้ากับการส่งคลื่นความถี่แทน DAC (จากบทความที่แล้ว) และการหรี่หลอดแอลอีดี โดยใช้บอร์ดทดลองดังภาพที่ 1
โครงสร้างของโครงงาน
โครงสร้างของโครงงานของ ESP-IDF เป็นดังภาพที่ 2 คือ ในไดเร็กทอรีหรือโฟลเดอร์ของโครงงานจะมีไฟล์ CMakeList.txt และ sdkconfig กับไดเร็กทอรีชื่อ main สำหรับเก็บรหัสต้นฉบับของโครงงาน โดยในไดเร็กทอรีดังกล่าวมีไฟล์ภาษา C และ CMakeLists.txt
จากโครงสร้างในภาพที่ 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 หลังจากนั้นบันทึกและออกจากการตั้งค่า
ที่มักจะลืมกันคือตั้งค่าขนาดความจุของหน่วยความจำรอม (Flash size) ดังภาพที่ 4 ให้ตรงกับขนาดที่ติดตั้งบนบอร์ด จากเสนู Serial flasher config ซึ่งในบทความใช้เป็น 4MB หลังจากนั้นกด S และ Q เพื่อบันทึกและออกจากการตั้งค่า
LEDC
espressif ใช้คลาส LEDC เป็นโมดูลควบคุมความเข้มของหลอดแอลอีดีเป็นหลัก โดยโมดูลนี้ใช้ PWM เป็นตัวขับแรงดันที่ต่อกับหลอดแอลอีดี และสามารถใช้ได้ 16 ช่องสัญญาณที่สร้างลูกคลื่นได้อิสระจากกัน โดยแบ่งเป็น 2 ประเภท ดังภาพที่ 5 คือ
- กลุ่มที่ทำงานแบบ High Speed Channel
- กลุ่มที่ทำงานแบบ Low Speed Channel
การใช้งาน PWM หรือ LEDC ประกอบด้วย 3 ขั้นตอนดังนี้ (ดังภาพที่ 6)
- ตั้งค่าตัวตั้งเวลา
- ตั้งค่าช่องสัญญาณ
- เปลี่ยนสัญญาณของ PWM
การตั้งค่าตัวตั้งเวลา
เป็นการกำหนดค่าให้กำหนดโครงสร้างข้อมูลประเภท 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()
การทำงานในด้านของค่าดิวตี้นั้นจะใช้หลักการค่อยเปลี่ยนจากค่าหนึ่งไปยังอีกค่าหนึ่ง ซึ่งคำสั่งที่เกี่ยวข้องสำหรับการตั้งค่าการเฟดมีดังนี้
- ledc_fade_start( ledc_mode_t speed_mode, ledc_channel_t chennel, ledc_fade_mode_t fade_mode ) สำหรับเริ่มต้นทำงาน
- 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 มิลลิวินาที
- 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
- 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 แบบอื่น ๆ สามารถเข้าไปอ่านบทความต่าง ๆ ดังต่อไปนี้เพิ่มเติม
ตัวอย่างโปรแกรม
ตัวอย่างโปรแกรมที่ esp-idf เตรียมไว้ให้มี 2 ตัวคือ
อุปกรณ์
อุปกรณ์ที่ใช้ในการทดลองได้แก่
- บอร์ด esp32
- บอร์ดทดลอง
- หลอดแอลอีดี และตัวต้านทาน
- โมดูลลำโพง
- ทรานซิสเตอร์
- ตัวต้านทาน 2 ตัว
- บัซเซอร์
โค้ดโปรแกรม
ตัวอย่างโปรแกรมสร้างคลื่นความถี่ 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
สรุป
จากบทความนี้จะพบว่า การการใช้ PWM หรือ LEDC นั้นประกอบด้วย 3 ขั้นตอน คือ
- ตั้งค่าตัวตั้งเวลา ด้วย ledc_timer_config()
- ตั้งค่าช่องสัญญาณ ledc_channel_config()
- สั่งงาน ledc_set_duty(), ledc_update_duty() และ ledc_stop()
แต่อย่างไรก็ดี ตัวอย่างนี้ครั้งนี้เป็นเฉพาะเรื่องการใช้ PWM แบบไม่ได้ใช้ฮาร์ดแวร์ในส่วนของ Timer เพื่อทำการเฟดค่า แต่ในตัวอย่างหลอดแอลอีดีนั้นได้ทำการเฟดค่าด้วยการเขียนโปรแกรมเพื่อวนรอบ แต่อย่างไรก็ดี เมื่อทางทีมงานเราได้เขียนบทความการขัดจังหวะ และการใช้งานตัวตั้งเวลาเป็นที่เรียบร้อยแล้วจะกลับมาทำตัวอย่างสำหรับเรื่องนี้อีกครั้ง และสุดท้ายขอให้สนุกกับการเขียนโปรแกรมครับ
ท่านใดต้องการพูดคุยสามารถคอมเมนท์ได้เลยครับ
แหล่งอ้างอิง
- WiKiPedia : Pulse-width modulation
- ESP-IDF : LEDC
(C) 2020-2021, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-10-13, 2021-12-27