การสร้างสัญญาณพัลส์และวัดความถี่โดยใช้ Arduino-ESP32#
▷ Pulse Generation & Frequency Measurement#
บทความนี้กล่าวถึง ตัวอย่างการเขียนโค้ด Arduino สำหรับบอร์ดไมโครคอนโทรลเลอร์ ESP32 เพื่อสาธิตการสร้างสัญญาณพัลส์ต่อเนื่องที่มีคาบหรือความถี่คงที่ (Periodic Signals) และการนับจำนวนพัลส์ในช่วงเวลาหนึ่งสำหรับการนำไปคำนวณค่าความถี่ของสัญญาณ
การสร้างสัญญาณพัลส์และสัญญาณที่มีคาบสำหรับ ESP32 ทำได้หลายวิธี เช่น
- การใช้ไลบรารี ESP32 Ticker
- การใช้วงจร LEDC (LED Control) ของชิป ESP32
- การใช้วงจร RMT (Remote Control Transceiver) ของชิป ESP32
การวัดความถี่ของสัญญาณที่มีคาบ หรือนับจำนวนพัลส์ในหนึ่งช่วงเวลา ก็สามารถใช้เทคนิคที่แตกต่างกันหลายวิธี เช่น
- การใช้คำสั่ง
pulseIn()
ของ Arduino API เพื่อวัดความกว้างของพัลส์ - การตรวจสอบขอบขาขึ้นหรือขาลงของสัญญาณอินพุต และเปิดใช้งานอินเทอร์รัพท์ภายนอก เพื่อนับเหตุการณ์ที่เกิดขึ้น
- การใช้วงจร Pulse Counter (PCNT) ของชิป ESP32
สำหรับการทดสอบโค้ดโดยใช้ฮาร์ดแวร์จริง ได้เลือกใช้บอร์ด WeMos LOLIN32 Lite ซึ่งเป็นบอร์ดไมโครคอนโทรลเลอร์ ESP32 ที่มีราคาไม่แพง
ข้อสังเกต: เนื่องจาก Arduino-ESP32 Core มีหลายเวอร์ชัน เช่น v2.0.x และในขณะที่อัปเดตบทความนี้ ได้มีการพัฒนาเวอร์ชัน v3.0.x ซึ่งใหม่กว่า และมีเปลี่ยนแปลงในส่วนของ API และฟังก์ชันต่าง ๆ ที่เกี่ยวข้องกับการใช้งานวงจรภายใน โดยอ้างอิงฟังก์ชันการทำงานของ ESP-IDF v5.1 (หรือสูงกว่า) รูปแบบการเขียนโค้ดจึงแตกต่างจากแบบเดิม ดังนั้นโค้ดตัวอย่างจึงแบ่งสองกรณี และแนะนำให้ปรับเปลี่ยนไปใช้เวอร์ชันใหม่กว่าในอนาคต
▷ Ticker-based Pulse Generation#
โค้ดตัวอย่างนี้ สาธิตการสร้างสัญญาณที่เป็นพัลส์ โดยใช้ไลบรารี Ticker ที่เป็นส่วนหนึ่งของ Arduino-ESP32 Core การทำงานของ Ticker เกี่ยวข้องกับการใช้งานวงจร General-Purpose Hardware Timer ภายในชิป ESP32 มีโหมดการทำงานให้เลือกใช้ 2 โหมด คือ ทำซ้ำ (Periodic) และ ทำครั้งเดียว (Once หรือ One-Shot)
ในตัวอย่างนี้ ได้เลือกใช้โหมดทำซ้ำ เพื่อเรียกใช้ฟังก์ชัน User-defined Callback ด้วยอัตราคงที่ เช่น
ฟังก์ชัน ticker_callback()
ที่ทำหน้าที่สลับสถานะลอจิกที่ขาเอาต์พุต LED_PIN
ทุก ๆ 250
มิลลิวินาที
หากต้องการจะวัดความกว้างของพัลส์ช่วงที่เป็น High (ลอจิก '1') ก็ให้ใช้ลวดสายไฟ
เชื่อมต่อจากขา GPIO22 (เอาต์พุต) ไปยังขา GPIO5 (อินพุต)
และใช้คำสั่ง pulseIn(...)
ของ Arduino API เพื่อวัดค่าความกว้างของพัลส์ที่ขาอินพุตดังกล่าว
และจะได้ค่าตัวเลขในหน่วยเป็นไมโครวินาที
// This Arduino sketch works with both Arduino-ESP32 v2.0.x and v3.0.0.
#include <Ticker.h> // Import the Ticker library
// Note: Connect GPIO22 (output) to GPIO5 (input)
#define LED_PIN (22)
#define INPUT_PIN (5)
#define INTERVAL_MSEC (250)
Ticker ticker; // Create a Ticker object
void setup() {
Serial.begin( 115200 );
pinMode( INPUT_PIN, INPUT_PULLUP );
pinMode( LED_PIN, OUTPUT );
// Attach a callback function (ISR) to the Ticker.
ticker.attach_ms( INTERVAL_MSEC /*msec*/, ticker_callback );
}
void loop() {
// Measure the high pulse width of the input signal.
uint32_t pw = pulseIn( INPUT_PIN /*GPIO pin*/,
1 /*logic level*/,
1000000 /*timeout in usec*/ );
// Show the measured pulse width.
Serial.printf( "Pulse width: %lu usec\n", (unsigned long) pw );
delay(1000);
}
void ticker_callback() { // The callback function for Ticker
// Toggle the LED output.
digitalWrite( LED_PIN, !digitalRead( LED_PIN ) );
}
รูป: การจำลองการทำงานโดยใช้ Wokwi Simulator
รูป: การคอมไพล์และอัปโหลดโค้ดโดยใช้ Arduino IDE 2.0.3 และรับข้อความจากบอร์ด ESP32 (วัดความกว้างของพัลส์ได้ 250000 ไมโครวินาที)
รูป: บอร์ด ESP32 (WeMos Lolin32 Lite) ที่มีการต่อสายไฟจากขา GPIO5 ไปยัง GPIO22
▷ Timer-based Pulse Generation#
โค้ดตัวอย่างนี้ สาธิตการใช้งาน Hardware Timer ของ ESP32
และเขียนโค้ดโดยใช้คำสั่งของ Arduino-ESP32 Timer API
หากต้องการศึกษารายละเอียด ก็สามารถดูได้จากไฟล์
cores/esp32/esp32-hal-timer.c
ในโค้ดตัวอย่างนี้ มีการตั้งค่าและใช้งาน Hardware Timer
โดยใช้คำสั่ง timerBegin(...)
เพื่อเลือกใช้ Timer หมายเลข 0 (จากทั้งหมด 4 ตัว ที่ให้เลือกใช้งานได้)
ตั้งค่าตัวหารความถี่เท่ากับ 80 ดังนั้นจะได้ความถี่ในการนับคำนวณได้จาก
APB Clock Frequency (80MHz default) หารด้วย 80 ได้เท่ากับ 1MHz หรือ 1 usec per Tick
นอกจากนั้นยังมีการเปิดใช้งานฟังก์ชัน Alarm ของ Timer
โดยใช้คำสั่ง timerAlarmWrite(...)
เพื่อให้เกิดเหตุการณ์อินเทอร์รัพท์ เช่น ทุก ๆ 1000 ไมโครวินาที
และใช้คำสั่ง timerAttachInterrupt(...)
เพื่อกำหนดให้ฟังก์ชันชื่อ timer_isr()
เป็นฟังก์ชัน ISR หรือ Callback Function คอยเพิ่มค่าของตัวแปรภายนอก
tick_counter
ครั้งละหนึ่ง เมื่อเกิดอินเทอร์รัพท์
// This Arduino sketch works with both Arduino-ESP32 v2.0.x and v3.0.0.
// https://github.com/espressif/arduino-esp32/blob/master/
// -> cores/esp32/esp32-hal-timer.h
// -> cores/esp32/esp32-hal-timer.c
#define ESP_TIMER_NUMBER (0)
// Declare a global variable
static uint32_t tick_counter = 0;
// Declare a variable to keep the timer struct
hw_timer_t *timer = NULL;
// Use a FreeRTOS mutex to protect the shared variable (tick_counter)
portMUX_TYPE timer_mux = portMUX_INITIALIZER_UNLOCKED;
void IRAM_ATTR timer_isr() {
portENTER_CRITICAL_ISR( &timer_mux );
tick_counter++; // Increment the tick counter
portEXIT_CRITICAL_ISR( &timer_mux );
}
void setup() {
Serial.begin(115200);
Serial.println("\n\nESP32 Hardware Timer Demo...");
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
timer = timerBegin( 1000000 );
if (timer == NULL){
Serial.println("Timer init failed!!");
}
timerAlarm( timer, 1000 /*period (ticks)*/,
true /*autoreload*/, 0 /*autoreload count*/ );
timerAttachInterrupt( timer, &timer_isr );
timerStart( timer );
#else
// Use the first hardware timer (0)
// Set prescaler to 80 -> Timer frequency = 80MHz/80 (1usec per tick)
// Note: The start will start automatically.
timer = timerBegin( ESP_TIMER_NUMBER /*timer number*/,
80 /*prescaler*/, true /*count up*/ );
// Attach the callback function (ISR) to the timer
timerAttachInterrupt( timer, &timer_isr, true );
// Set alarm to call the ISR function every 1msec
timerAlarmWrite( timer, 1000 /*period (ticks)*/,
true /*autoreload*/ );
// Start the alarm
timerAlarmEnable( timer );
// Reset counter value of the timer to 0
timerWrite( timer, 0 /*value*/ );
#endif
//timerStop( timer );
//timerRestart( timer );
//timerDetachInterrupt( timer );
//timerEnd( timer );
}
void loop() {
portENTER_CRITICAL( &timer_mux );
uint32_t value = tick_counter; // Read the current tick counter
portEXIT_CRITICAL( &timer_mux );
Serial.printf( "Ticks: %lu\n", (unsigned long) value );
delay(1000);
}
รูป: ตัวอย่างการทดสอบโค้ดโดยใช้บอร์ด ESP32 และข้อความเอาต์พุตที่ได้จากการทำงาน
ถัดไปเป็นการแก้ไขโค้ดตัวอย่าง เพื่อสร้างสัญญาณเอาต์พุตสำหรับ LED และวัดความกว้างของพัลส์ของสัญญาณอินพุต โดยใช้ขา GPIO22 เป็นเอาต์พุต และจะต้องเชื่อมต่อด้วยสายไฟไปยังขา GPIO5 เพื่อใช้เป็นขาอินพุตและวัดความกว้างของพัลส์
การทำงานของโค้ดตัวอย่าง จะทำให้เกิดการสลับสถานะลอจิกขา GPIO22 ทุก ๆ 100 มิลลิวินาที
และค่าความกว้างของพัลส์สามารถวัดได้ โดยใช้คำสั่ง pulseIn(...)
// This Arduino sketch works with both Arduino-ESP32 v2.0.x and v3.0.0.
// Note: Connect GPIO22 (output) to GPIO5 (input)
#define LED_PIN (22)
#define INPUT_PIN (5)
#define ESP_TIMER_NUMBER (0)
#define INTERVAL_MSEC (100)
// Declare a global variable
static uint32_t tick_counter = 0;
// Declare a variable to keep the timer struct
hw_timer_t *timer = NULL;
void IRAM_ATTR timer_isr() {
tick_counter++; // Increment the tick counter
if ( tick_counter == INTERVAL_MSEC ) {
// Toggle the LED
digitalWrite( LED_PIN, !digitalRead(LED_PIN) );
// Reset the tick counter
tick_counter = 0;
}
}
void initTimer() {
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
timer = timerBegin( 1000000 );
if (timer == NULL){
Serial.println("Timer init failed!!");
}
timerAlarm( timer, 1000 /*period (ticks)*/,
true /*autoreload*/, 0 /*autoreload count*/ );
timerAttachInterrupt( timer, &timer_isr );
timerStart( timer );
#else
// Use the first hardware timer (0)
// Set prescaler to 80 -> Timer frequency = 80MHz/80 (or 1usec/tick)
// Note: The start will start automatically.
timer = timerBegin( ESP_TIMER_NUMBER /*timer number*/,
80 /*prescaler*/, true /*count up*/ );
// Attach the callback function (ISR) to the timer
timerAttachInterrupt( timer, &timer_isr, true );
// Set alarm to call the ISR function every 1msec
timerAlarmWrite( timer, 1000 /*period (ticks)*/, true /*autoreload*/ );
// Start the alarm
timerAlarmEnable( timer );
#endif
}
void setup() {
Serial.begin(115200);
Serial.println("\n\nESP32 Hardware Timer Demo...");
pinMode( LED_PIN, OUTPUT );
pinMode( INPUT_PIN, INPUT_PULLUP );
initTimer();
}
void loop() {
// Measure the pulse width (in usec)
uint32_t pw = pulseIn( INPUT_PIN, 1, 1000000 );
Serial.printf( "Pulse width: %lu usec\n", (unsigned long) pw );
delay(1000);
}
รูป: ตัวอย่างการทดสอบโค้ดโดยใช้บอร์ด ESP32 และข้อความเอาต์พุตที่ได้จากการทำงาน (วัดความกว้างของพัลส์ได้ 100000 ไมโครวินาที หรือ 100 มิลลิวินาที)
▷ RMT-based Pulse Generation#
โค้ดตัวอย่างนี้ สาธิตการสร้างสัญญาณแบบพัลส์ โดยใช้วงจร RMT จำนวนหนึ่งช่องสัญญาณ (เรียกว่า RMT TX Channel) ทำหน้าที่เป็นตัวส่งสัญญาณพัลส์หนึ่งลูกคลื่นออกไป และทำซ้ำหรือวนลูปด้วยอัตราคงที่
ตัวแปร rmt_tx_cfg
ที่มีชนิดข้อมูลเป็น rmt_config_t
จะถูกใช้ในการตั้งค่าสำหรับการทำงานของ
RMT จำนวนหนึ่งช่องสัญญาณ (เลือกใช้ RMT_CHANNEL_0
) เมื่อเรียกใช้ฟังก์ชัน
rmt_config(...)
และเลือกใช้ขา GPIO22 เป็นเอาต์พุต
การส่งสัญญาณออกไปโดยใช้ RMT จะต้องมีการกำหนดรูปแบบของข้อมูลในรูปแบบที่เรียกว่า RMT Symbol ซึ่งจะประกอบด้วยช่วงที่เป็นลอจิก 1 และช่วงที่เป็นลอจิก 0 พร้อมกำหนดความกว้างของแต่ละช่วง
วงจร RMT ภายในชิป ESP32 ทำงานด้วยสัญญาณ CLK ที่มีความถี่ 80MHz หรือใช้ความถี่ต่ำกว่าได้ โดยการกำหนดค่าตัวหารความถี่ ในตัวอย่างนี้ได้เลือกค่าตัวหารความถี่เท่ากับ 80 ดังนั้น RMT จะทำงานด้วยความถี่ 80MHz/80 หรือ 1MHz หรือ 1usec ต่อหนึ่งไซเคิล (เรียกว่า RMT tick)
ในโค้ดตัวอย่าง มีการใช้ตัวแปร pulse_item
ที่มีชนิดข้อมูลเป็น rmt_item32_t
และการเรียกใช้ฟังก์ชัน rmt_write_items(...)
ทำหน้าที่กำหนดความกว้างของพัลส์
ช่วงที่เป็นลอจิก 1 และช่วงที่เป็นลอจิก 0 เช่น เท่ากับ 500
(เป็นข้อมูลแบบเลขจำนวนเต็ม 15 บิต) ซึ่งหน่วยเป็น 1 ไมโครวินาที
ถ้าวัดสัญญาณเอาต์พุตที่ได้ จะมีความถี่เท่ากับ 1kHz หรือ มีคาบเท่ากับ 1000 usec
วงจร RMT รองรับการมอดูเลตสัญญาณ (Signal Modulation) ด้วยสัญญาณพาหะ (Carrier Signal) ที่มีความถี่สูงกว่า เช่น ในกรณีที่ต้องการสร้างสัญญาณสำหรับอุปกรณ์รีโมตอินฟราเรด แต่ในตัวอย่างนี้ ไม่มีการมอดูเลตสัญญาณเอาต์พุต
// This Arduino sketch works with both Arduino-ESP32 v2.0.x and v3.0.0.
// https://github.com/espressif/arduino-esp32/blob/master/
// -> cores/esp32/esp32-hal-rmt.h
// -> cores/esp32/esp32-hal-rmt.c
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
#include <driver/rmt_common.h>
#include <driver/rmt_tx.h>
#include <hal/rmt_types.h>
#else
#include <driver/rmt.h> // Required for RMT
#endif
// Note: Connect GPIO22 (output) to GPIO5 (input)
#define INPUT_PIN (GPIO_NUM_5)
#define RMT_GPIO_PIN (GPIO_NUM_22)
#define RMT_CHANNEL (RMT_CHANNEL_0)
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
rmt_channel_handle_t tx_chan = NULL;
const rmt_tx_channel_config_t tx_chan_config = {
.gpio_num = RMT_GPIO_PIN, // GPIO number
.clk_src = RMT_CLK_SRC_DEFAULT, // select source clock
.resolution_hz = 1 * 1000 * 1000, // 1 MHz tick resolution, i.e., 1 tick = 1 µs
.mem_block_symbols = 64, // memory block size, 64 * 4 = 256 Bytes
.trans_queue_depth = 4, // set the number of transactions
.flags = {
.invert_out = false, // do not invert output signal
.with_dma = false,
.io_loop_back = false,
.io_od_mode = false,
},
.intr_priority = 0,
};
const rmt_transmit_config_t tx_config = {
.loop_count = -1, // transmit in an infinite loop.
.flags = {
.eot_level = 0,
},
};
// Define the RMT item for a 1kHz square wave
const rmt_symbol_word_t pulse_rmt_symbol = {
{
.duration0 = 500, // High pulse width in cycles
.level0 = 1, // High level
.duration1 = 500, // Low pulse width in cycles
.level1 = 0 // Low level
}
};
void initRMT( ) {
ESP_ERROR_CHECK( rmt_new_tx_channel(&tx_chan_config, &tx_chan));
// The functionality of a copy encoder is to copy the RMT symbols
// from user space into the driver layer.
rmt_encoder_handle_t copy_encoder = NULL;
rmt_copy_encoder_config_t copy_encoder_config = {};
ESP_ERROR_CHECK( rmt_new_copy_encoder(©_encoder_config, ©_encoder) );
ESP_ERROR_CHECK( rmt_enable(tx_chan) );
ESP_ERROR_CHECK( rmt_transmit(tx_chan, copy_encoder,
&pulse_rmt_symbol, sizeof(pulse_rmt_symbol), &tx_config) );
}
#else
// Define the RMT configuration parameters
// - The RMT clock is set to 80MHz/80 = 1MHz (1usec per cycle).
// - The carrier frequency is not used (no signal modulation).
const rmt_config_t rmt_tx_cfg = {
.rmt_mode = RMT_MODE_TX, // RMT mode: transmitter
.channel = RMT_CHANNEL, // RMT channel to use
.gpio_num = RMT_GPIO_PIN, // GPIO number to output the signal
.clk_div = 80, // RMT clock divider (80MHz/80 = 1MHz)
.mem_block_num = 1, // Number of memory blocks to use
.tx_config = { // Configuration for TX
.carrier_level = RMT_CARRIER_LEVEL_HIGH, // Carrier level
.idle_level = RMT_IDLE_LEVEL_LOW, // Set idle level to low
.carrier_duty_percent = 50, // Carrier duty cycle
.carrier_en = false, // Disable carrier
.loop_en = true, // Enable loop mode
.idle_output_en = true, // Enable idle level
}
};
// Define the RMT item for a 1kHz square wave
const rmt_item32_t pulse_item = {
{{
.duration0 = 500, // High pulse width in cycles
.level0 = 1, // High level
.duration1 = 500, // Low pulse width in cycles
.level1 = 0 // Low level
}}
};
void initRMT( ) {
// Initialize RMT for TX mode
ESP_ERROR_CHECK( rmt_config( &rmt_tx_cfg ) );
ESP_ERROR_CHECK( rmt_driver_install(RMT_CHANNEL, 0, 0) );
// Write pulse data and wait for RMT TX done
rmt_write_items( RMT_CHANNEL, &pulse_item, 1, true );
}
#endif
void setup() {
Serial.begin( 115200 );
pinMode( INPUT_PIN, INPUT_PULLUP );
initRMT();
}
void loop() {
// Pulse width measurement
uint32_t pw = pulseIn( INPUT_PIN /*GPIO pin*/,
1 /*logic level: high pulse*/,
1000000 /* timeout in usec*/ );
Serial.printf( "Pulse width: %lu usec\n", (unsigned long) pw );
Serial.printf( "Frequency: %.3lf kHz\n", 1000.0/(2*pw) );
delay(1000);
}
รูป: การจำลองการทำงานของโค้ดด้วย Wokwi Simulator (วัดความถี่ของสัญญาณ 1kHz)
รูป: การคอมไพล์และอัปโหลดโค้ดโดยใช้ Arduino IDE และรับข้อความจากบอร์ด ESP32 (วัดความถี่ของสัญญาณ 1kHz)
รูป: การวัดสัญญาณด้วยออสซิลโลสโคป (RIGOL DS1054Z) และได้ค่าความถี่ 1kHz
▷ LEDC-based Pulse Generation#
วงจร LEDC ภายในชิป ESP32 (ซึ่งมีทั้งหมด 16 ช่องเอาต์พุต) เหมาะสำหรับการสร้างสัญญาณแบบ PWM (Pulse Width Modultion) ที่สามารถปรับค่า Duty Cycle (ความกว้างของพัลส์ช่วงที่มีลอจิกเป็น High) และนำไปใช้ในการปรับความสว่างของ LED โค้ดตัวอย่างนี้ สาธิตการสร้างสัญญาณแบบพัลส์ โดยใช้ LEDC ช่องหมายเลข 0
ฟังก์ชันที่สำคัญและเกี่ยวข้องกับการทำงานของ LEDC ได้แก่
uint32_t ledcSetup(uint8_t channel, uint32_t freq, uint8_t bits)
: ตั้งค่าการใช้งาน LEDC สำหรับช่องหมายเลขchannel
ระบุความถี่freq
(หน่วยเป็น Hz) และความละเอียดหรือจำนวนบิตbits
(เลือกได้ในช่วง 1 - 20 บิต) สำหรับการตั้งค่า Duty Cyclevoid ledcAttachPin(uint8_t pin, uint8_t channel)
: เลือกใช้ขาpin
สำหรับช่องเอาต์พุตchannel
ของ LEDCvoid ledcWrite(uint8_t chan, uint32_t duty)
: กำหนดค่า Duty Cycle สำหรับช่องเอาต์พุตchannel
ในโค้ดตัวอย่างนี้ ได้เลือกใช้จำนวนบิต เท่ากับ 8 บิต และความถี่เท่ากับ 100 kHz เพื่อสร้างสัญญาณ PWM โดยใช้ LEDC (Channel 0) ที่ขา GPIO22 แต่ถ้าต้องการได้ความถี่ที่สูงขึ้น จะต้องลดจำนวนบิตที่ใช้สำหรับการระบุค่า Duty Cycle
การนับจำนวนพัลส์ที่เกิดขึ้น จะใช้วิธีการตรวจจับขอบขาขึ้น (Rising Edge)
และเปิดใช้งานอินเทอร์รัพท์ภายนอก โดยใช้คำสั่ง attachInterrupt(...)
ของ Arduino API
และสร้างฟังก์ชัน pulse_isr()
เพื่อใช้เป็น ISR และถูกเรียกใช้โดยอัตโนมัติ เมื่อเกิดอินเทอร์รัพท์ที่เกี่ยวข้อง
แม้ว่าวิธีนี้จะใช้นับจำนวนพัลส์ในช่วงเวลาที่กำหนดได้ แต่ก็มีข้อจำกัดเชิงเวลาในการตอบสนองต่อการเกิดอินเทอร์รัพท์ภายนอก (Interrupt Latency) ดังนั้นถ้าสัญญาณอินพุตมีความถี่สูง (เช่น สูงกว่า 200 kHz) ก็จะทำให้ค่าที่วัดได้ผิดพลาดจากค่าที่เป็นจริง
ในตัวอย่างนี้ สัญญาณเอาต์พุตที่ขา GPIO22 จะถูกป้อนกลับเข้าที่ขา GPIO5 หากให้เวลานับจำนวนพัลส์ที่เกิดขึ้นในช่วงเวลา 1000 มิลลิวินาที (หรือต่อหนึ่งวินาที) ก็สามารถนำค่าที่ได้มาคำนวณเป็นค่าความถี่ของสัญญาณ
// This Arduino sketch works with both Arduino-ESP32 v2.0.x and v3.0.0.
// https://github.com/espressif/arduino-esp32/blob/master/
// -> cores/esp32/esp32-hal-ledc.h
// -> cores/esp32/esp32-hal-ledc.c
// Note: Connect GPIO22 (output) to GPIO5 (input)
#define INPUT_PIN (5)
#define LEDC_GPIO_PIN (22)
#define LEDC_CHANNEL (0)
#define NUM_BITS (8)
#define FREQ_HZ (1000UL)
// Use a FreeRTOS mutex to protect the shared variable (tick_counter)
portMUX_TYPE timer_mux = portMUX_INITIALIZER_UNLOCKED;
static uint32_t count = 0;
static uint32_t last_time_msec = 0;
void IRAM_ATTR pulse_isr() { // ISR to handle pulse counter events
portENTER_CRITICAL_ISR( &timer_mux );
count++; // Increment the pulse counter
portEXIT_CRITICAL_ISR( &timer_mux );
}
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
void initLEDC() {
// Setup LEDC pin with given frequency and resolution.
ledcAttach( LEDC_GPIO_PIN, FREQ_HZ, NUM_BITS );
// Set the duty cycle for the PWM output (50%).
ledcWrite( LEDC_GPIO_PIN, (1<<(NUM_BITS-1))-1 );
delay(10);
// Read the PWM frequency.
Serial.printf( "LEDC Freq.: %lu Hz\n",
(unsigned long) ledcReadFreq(LEDC_GPIO_PIN) );
}
#else
void initLEDC() {
// see: cores/esp32/esp32-hal-ledc.c
// Initialize the LEDC channel 0, use 8-bit resolution.
ledcSetup( LEDC_CHANNEL, FREQ_HZ, NUM_BITS );
// Attach the GPIO22 pin to the LEDC Channel 0.
ledcAttachPin( LEDC_GPIO_PIN, LEDC_CHANNEL );
// Set the duty cycle for the PWM output (50%).
ledcWrite( LEDC_CHANNEL, (1<<(NUM_BITS-1))-1 );
delay(10);
// Read the PWM frequency.
Serial.printf( "LEDC Freq.: %lu Hz\n",
(unsigned long) ledcReadFreq(LEDC_GPIO_PIN) );
}
#endif
void setup() {
// Initialize serial communication.
Serial.begin( 115200 );
// Set the LED pin as an output.
pinMode( LEDC_GPIO_PIN, OUTPUT );
// Set the input pin for the pulse counter.
pinMode( INPUT_PIN, INPUT_PULLUP );
// Attach the ISR to the input pin.
attachInterrupt( INPUT_PIN, pulse_isr, RISING );
initLEDC();
count = 0;
last_time_msec = millis();
}
void loop() {
static uint32_t saved_count;
uint32_t now = millis();
if ( now - last_time_msec >= 1000 ) { // Print count every 1 sec.
portENTER_CRITICAL( &timer_mux );
saved_count = count;
// Reset the pulse count.
count = 0;
portEXIT_CRITICAL( &timer_mux );
if (saved_count >= 1000 ) {
// Calculate the frequency in kHz.
float frequency = saved_count/1000.0;
Serial.printf( "Frequency: %.3f kHz\n", frequency );
} else {
// Calculate the frequency in Hz.
int frequency = saved_count;
Serial.printf( "Frequency: %d Hz\n", frequency );
}
// Save the timestamp
last_time_msec = now;
}
}
รูป: การจำลองการทำงานด้วย Wokwi Simulator (สัญญาณเอาต์พุตที่มีความถี่ 1kHz)
การสร้างสัญญาณ PWM สำหรับ ESP32 ก็อาจจะใช้คำสั่งของ Arduino-ESP32 API (v2.0.x) ได้ดังนี้
// PWM frequency 1kHz
analogWriteFrequency( 1000 ); // Frequency: 1kHz
analogWriteResolution( 8 ); // Resolution: 8-bit
analogWrite( LEDC_GPIO_PIN, 127 ); // Duty cycle: 50%
รูป: การคอมไพล์และอัปโหลดโค้ดโดยใช้ Arduino IDE และรับข้อความจากบอร์ด ESP32 (สร้างสัญญาณเอาต์พุตที่มีความถี่ 100 kHz)
ถ้าจะใช้ความถี่สูง เช่น 10MHz จะต้องลดจำนวนบิตลง เช่น มีขนาดเพียง 2 บิต ตามตัวอย่างต่อไปนี้
// LEDC Channel 0, 10MHz, 2-bit resolution
ledcSetup( LEDC_CHANNEL, 10000000UL /*Hz*/, 2 /*bits*/ );
// Attach the GPIO22 pin to LEDC Channel 0
ledcAttachPin( LEDC_GPIO_PIN, LEDC_CHANNEL );
// Set the PWM duty cycle (~50%)
ledcWrite( LEDC_CHANNEL, 2 );
รูป: ตัวอย่างรูปคลื่นสัญญาณ 10MHz เมื่อวัดด้วยออสซิลโลสโคป (RIGOL DS1054Z)
▷ ESP32 Pulse Counter#
ตัวอย่างถัดไปเป็นการสาธิตการใช้งานวงจร Pulse Counter (PCNT) ของชิป ESP32 เหมาะสำหรับการนับจำนวนพัลส์ของสัญญาณอินพุต หรือนำไปคำนวณค่าความถี่หากเป็นสัญญาณที่มีคาบ
ในตัวอย่างนี้มีการสร้างและใช้ฟังก์ชันชื่อ initPCNT()
เพื่อตั้งค่าให้ PCNT ช่องอินพุตหมายเลข 0
รับอินพุตจากขา GPIO22 และให้ตัวนับขนาด 16 บิต ของ PCNT นับขึ้นครั้งละหนึ่ง เมื่อเกิดขอบขาขึ้น
นอกจากนั้นแล้วยังตั้งค่าสูงสุดไว้ที่ 32767 เมื่อนับถึงค่าดังกล่าว จะทำให้เกิดอินเทอร์รัพท์
(Overflow Interrupt) และฟังก์ชันชื่อ overflow_isr()
จะถูกเรียกใช้โดยอัตโนมัติ ค่าของตัวแปรภายนอก pcnt_overflows
จะถูกเพิ่มขึ้นครั้งละหนึ่ง
เมื่อเกิดเหตุการณ์ดังกล่าว
การอ่านค่าปัจจุบันของตัวนับ PCNT จะใช้คำสั่ง pcnt_get_counter_value(...)
และถูกนำมาใช้ในการคำนวณจำนวนพัลส์ที่เกิดขึ้น โดยจะต้องใช้ค่า pcnt_overflows
ร่วมด้วย
คำสั่งต่าง ๆ ที่เกี่ยวข้องกับการใช้งาน PCNT สามาารถดูได้จาก ESP-IDF Programming Guide - PCNT API
// This Arduino sketch works with both Arduino-ESP32 v2.0.x and v3.0.0.
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/pcnt.html
// Settings for PCNT
#define PCNT_INPUT_PIN (5)
#define PCNT_UNIT PCNT_UNIT_0
#define PCNT_H_LIM_VAL (32767)
#define PCNT_L_LIM_VAL (0)
// Settings for PWM generation
#define PWM_PIN (22)
#define PWM_FREQ (100000UL)
#define NUM_BITS (2)
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
#include "driver/pulse_cnt.h"
#include "driver/gpio.h"
#else
#include "driver/periph_ctrl.h"
#include "driver/pcnt.h"
#include "soc/pcnt_struct.h"
#include "driver/gpio.h"
#include "driver/ledc.h"
#endif
// Declare global variables
static uint32_t pcnt_overflows = 0;
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
static pcnt_unit_handle_t pcnt_unit = NULL;
static pcnt_channel_handle_t pcnt_chan = NULL;
// The internal hardware counter will be cleared to zero automatically
// when it reaches high or low limit.
static bool IRAM_ATTR pcnt_on_reach( pcnt_unit_handle_t unit,
const pcnt_watch_event_data_t *edata, void *user_ctx )
{
pcnt_overflows++;
return false;
}
void initPCNT() {
// Create and configure a PCNT unit.
pcnt_unit_config_t unit_config = {
.low_limit = PCNT_L_LIM_VAL-1,
.high_limit = PCNT_H_LIM_VAL,
.flags = { .accum_count = 0 },
};
ESP_ERROR_CHECK( pcnt_new_unit(&unit_config, &pcnt_unit) );
// Set glitch filter.
pcnt_glitch_filter_config_t filter_config = {
.max_glitch_ns = 100, // Set the maximum glitch width to 100 ns.
};
ESP_ERROR_CHECK( pcnt_unit_set_glitch_filter(pcnt_unit, &filter_config) );
pcnt_chan_config_t chan_config = {
.edge_gpio_num = PCNT_INPUT_PIN,
.level_gpio_num = -1,
};
ESP_ERROR_CHECK( pcnt_new_channel(pcnt_unit, &chan_config, &pcnt_chan) );
// Set specific actions for rising and falling edge of the signal channel.
ESP_ERROR_CHECK( pcnt_channel_set_edge_action( pcnt_chan,
PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_HOLD) );
// Set specific actions for high and low level of the signal channel.
ESP_ERROR_CHECK(
pcnt_channel_set_level_action( pcnt_chan,
PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_KEEP) );
// Add high limit watch point for the PCNT unit.
ESP_ERROR_CHECK( pcnt_unit_add_watch_point(pcnt_unit, PCNT_H_LIM_VAL));
// Register the callback function for the watchpoint event.
pcnt_event_callbacks_t cbs = {
.on_reach = pcnt_on_reach,
};
ESP_ERROR_CHECK( pcnt_unit_register_event_callbacks(pcnt_unit, &cbs, (void *)NULL) );
ESP_ERROR_CHECK( pcnt_unit_clear_count(pcnt_unit) );
// Enable the PCNT unit.
ESP_ERROR_CHECK( pcnt_unit_enable(pcnt_unit) );
// Start the PCNT unit.
ESP_ERROR_CHECK( pcnt_unit_start(pcnt_unit) );
}
uint32_t readPCNT() {
int pulse_count = 0;
pcnt_unit_get_count( pcnt_unit, &pulse_count );
pcnt_unit_clear_count( pcnt_unit );
uint32_t count = pulse_count + pcnt_overflows*(PCNT_H_LIM_VAL);
Serial.printf("overflows : %lu\n", pcnt_overflows );
pcnt_overflows = 0;
return count;
}
#else
//------------------------------------------------------------------------------------
void IRAM_ATTR overflow_isr(void *arg) {
PCNT.int_clr.val = BIT( PCNT_UNIT ); // Clear PCNT interrupt first.
pcnt_counter_clear( PCNT_UNIT ); // Clear the PCNT counter.
pcnt_overflows++; // Increment the overflow count.
}
//------------------------------------------------------------
void initPCNT() {
static pcnt_isr_handle_t pcnt_isr_handle = NULL;
// Configure the PCNT Channel 0 to operate in count-up mode.
pcnt_config_t pcnt_config;
pcnt_config.pulse_gpio_num = PCNT_INPUT_PIN;
pcnt_config.ctrl_gpio_num = -1;
pcnt_config.hctrl_mode = PCNT_MODE_KEEP;
pcnt_config.lctrl_mode = PCNT_MODE_KEEP;
pcnt_config.counter_h_lim = PCNT_H_LIM_VAL;
pcnt_config.counter_l_lim = PCNT_L_LIM_VAL-1;
pcnt_config.pos_mode = PCNT_COUNT_INC;
pcnt_config.neg_mode = PCNT_COUNT_DIS;
pcnt_config.unit = PCNT_UNIT;
pcnt_config.channel = PCNT_CHANNEL_0;
ESP_ERROR_CHECK( pcnt_unit_config( &pcnt_config ) );
// Configure the PCNT filter
pcnt_set_filter_value( PCNT_UNIT, 2 );
pcnt_filter_enable( PCNT_UNIT );
// Attach an ISR function to the PCNT interrupt
pcnt_counter_pause( PCNT_UNIT );
pcnt_counter_clear( PCNT_UNIT );
pcnt_isr_register( overflow_isr, NULL, 0, &pcnt_isr_handle );
pcnt_event_enable( PCNT_UNIT, PCNT_EVT_H_LIM );
pcnt_intr_enable( PCNT_UNIT );
pcnt_counter_resume( PCNT_UNIT );
}
//------------------------------------------------------------
uint32_t readPCNT() {
int16_t pcnt_count = 0;
pcnt_get_counter_value( PCNT_UNIT, &pcnt_count );
pcnt_counter_clear( PCNT_UNIT );
uint32_t count = ((uint32_t)pcnt_count) + pcnt_overflows*PCNT_H_LIM_VAL;
pcnt_overflows = 0;
return count;
}
#endif
void setup() {
Serial.begin( 115200 );
Serial.println( "\nESP32 PCNT Demo...." );
pinMode( PCNT_INPUT_PIN, INPUT_PULLUP );
// Initialize the PCNT.
initPCNT();
// Create a PWM signal for test purpose.
#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
// Set PWM duty cycle to 50%.
ledcAttach( PWM_PIN, PWM_FREQ, NUM_BITS );
ledcChangeFrequency( PWM_PIN, PWM_FREQ, NUM_BITS );
ledcWrite( PWM_PIN, (1<<NUM_BITS)/2 );
#else
ledc_channel_t ledc_channel = LEDC_CHANNEL_0;
ledcSetup( ledc_channel, PWM_FREQ /*Hz*/, NUM_BITS /*bits*/ );
// Attach the GPIO pin to LEDC Channel.
ledcAttachPin( PWM_PIN, ledc_channel );
// Set the PWM duty cycle (~50%).
ledcWrite( ledc_channel, (1<<NUM_BITS)/2 );
//analogWriteResolution( NUM_BITS ); // Set PWM resolution(bits).
//analogWriteFrequency( PWM_FREQ ); // Set PWM frequency.
//analogWrite( PWM_PIN, (1<<NUM_BITS)/2 ); // Set PWM duty cycle to 50%.
#endif
delay(1000);
}
void loop( ) {
static uint32_t last_time_msec = 0;
uint32_t now = millis(); // Read current time in msec
if ( now - last_time_msec >= 1000 ) { // Update every 1 sec.
uint32_t count = readPCNT(); // Read the current value of PCNT.
if ( count >= 1000 ) {
Serial.printf(" Freq. %.3f kHz\n", count / 1000.0 );
} else {
Serial.printf(" Freq. %lu Hz\n", (unsigned long) count );
}
last_time_msec = millis(); // Save the last update time.
}
}
ในกรณีที่สร้างสัญญาณ PWM ให้มีความถี่ 1MHz และ 8MHz ตามลำดับ จะได้ข้อความเอาต์พุตในลักษณะต่อไปนี้
รูป: ข้อความจากบอร์ด ESP32 ที่แสดงค่าความถี่ของสัญญาณที่วัดได้ (ได้ประมาณ 1000 kHz)
รูป: ข้อความจากบอร์ด ESP32 ที่แสดงค่าความถี่ของสัญญาณที่วัดได้ (ได้ประมาณ 8000 kHz)
รูป: การวัดสัญญาณเอาต์พุต (8MHz) โดยใช้ออสซิลโลสโคป (RIGOL DS1054Z)
▷ กล่าวสรุป#
บทความนี้ได้นำเสนอแนวทางการเขียนโค้ดสำหรับ Arduino-ESP32 เพื่อสร้างสัญญาณพัลส์ และวัดความกว้างของสัญญาณพัลส์ หรือความถี่ของสัญญาณที่มีคาบ โดยใช้วิธีที่แตกต่างกัน และเป็นวิธีที่เจาะจงใช้งานวงจรภายในของชิป ESP32
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Created: 2023-04-28 | Last Updated: 2023-10-27