การเขียนโปรแกรม ESP32-C6 ด้วย Arduino-ESP32 Core#


แนะนำชิป Espressif ESP32-C6#

ESP32-C6 Series (released: September 22, 2022) ของบริษัท Espressif Systems เป็นชิปที่มีซีพียู 32-bit RISC-V Core และมีคุณลักษณะด้านฮาร์ดแวร์ (ESP32-C6 Series Datasheet) เช่น

  • CPU Cores:
    • High-performance (HP Core): 32-bit RISC-V CPU, 160 MHz
    • Low-power (LP Core): 32-bit RISC-V CPU (ultra-low power consumption), 20MHz
      • LP Peripherals: LP IO, LP UART, LP I2C, ...
  • Storage:
    • L1 cache: 32 KB
    • ROM: 320 KB
    • SRAM: 512 KB (HP Core), 16KB (LP Core)
    • eFuse: 4 KBits
  • Packages (5×5 mm) / GPIOs:
    • QFN40 / 30 GPIOs
    • QFN32 / 22 GPIOs
  • I/O Drive Strength (Default): 20mA
  • Connectivity:
    • 2.4 GHz Wi-Fi 6 (IEEE 802.11b/g/n & 802.11ax)
    • 2.4 GHz Bluetooth 5 (LE) radio
    • 2.4 GHz IEEE 802.15.4-2015 (ZigBee 3.0 / Thread 1.3 / Matter compliant)
    • Built-in USB Serial/JTAG Controller (no USB-OTG)

เอกสารของผู้ผลิต

จุดเด่นที่น่าสนใจของชิป ESP32-C6 คือ การรองรับรูปแบบการสื่อสารไร้สายแบบหลายโพรโตคอล ด้วยคลื่นความถี่ 2.4GHz ได้แก่ BLE / ZigBee / Thread / Matter ซึ่งเหมาะสำหรับการใช้งานด้าน IoT / Smart Home

หากต้องการจะเขียนโปรแกรมสำหรับ LP RISC-V Core จะต้องใช้ ESP-IDF และสามารถศึกษาขั้นตอนการเขียนโปรแกรมได้จาก "ULP LP-Core Coprocessor Programming"

รูป: ESP32-C6 Block Diagram

บทความนี้นำเสนอตัวอย่างการเขียนโค้ด Arduino Sketch และการทดลองใช้งาน ESP32-C6 ในเบื้องต้น โดยใช้ Arduino-ESP32 Core v3.0.0+ (Released: November 2023) ซึ่งใช้ ESP-IDF v5.1 เป็นพื้นฐานในการพัฒนา และถือว่าเป็นการอัปเกรดเวอร์ชันครั้งสำคัญจาก Arduino ESP32 Core v2.0.0 (Released: September 2021) และรองรับการใช้งานชิปรุ่น ESP32-C6 และ ESP32-H2 จากเดิมที่มีเพียง ESP32 / ESP32-S2 / ESP32-S3 และ ESP32-C3

ผู้ใช้สามารถดูรายละเอียดเพิ่มเติมเกี่ยวกับการพัฒนาและการเปลี่ยนแปลงเวอร์ชันของ Arduino-ESP32 ในอดีตจนถึงปัจจุบันได้จาก https://github.com/espressif/arduino-esp32/releases

ในส่วนของซอฟต์แวร์ จะใช้งาน Arduino IDE v2.x สำหรับการเขียนโค้ด ดังนั้นจะต้องติดตั้ง Arduino ESP32 เวอร์ชันล่าสุด โดยระบุ URL สำหรับ Arduino Board Manager ดังนี้

https://espressif.github.io/arduino-esp32/package_esp32_dev_index.json

รูป: การติดตั้ง Arduino-ESP32 Core v3.0.0+ ใน Arduino IDE

ในส่วนของฮาร์ดแวร์ บอร์ด ESP32-C6 สำหรับการนำมาทดลองใช้งาน เช่น

รูป: แผนผังของบอร์ด ESP32-C6 DevKitC-1 (Schematic)

รูป: แผนผังแสดงองค์ประกอบสำคัญของบอร์ด ESP32-C6 DevKitC-1

จากแผนผังของบอร์ด ESP32-C6 DevKitC-1 (N4/N8) จะเห็นได้ว่า มีโมดูล ESP32-C6-WROOM-1 และในส่วนการเชื่อมต่อกับคอมพิวเตอร์ของผู้ใช้ มีคอนเนกเตอร์ USB Type-C แบ่งเป็น 2 ส่วน คือ

  • USB (ต่อตรงสัญญาณ USB D+/D- ที่ขา GPIO13 และ GPIO12 ตามลำดับ) ใช้สำหรับการทำงานของ USB-to-Serial & JTAG-over-USB
  • UART0 (ต่อสัญญาณจากขา U0TXD และ U0RXD ไปยังไอซี CP2102N: USB-to-UART Bridge) และตรงกับขา GPIO16 และ GPIO17 ตามลำดับ

การอัปโหลดไฟล์เฟิร์มแวร์ ผ่านทาง USB จึงมีสองทางเลือก

  • เชื่อมต่อผ่าน USB-to-Serial Bridge หรือ แต่เลือกใช้วิธีนี้ ให้เลือก USB CDC On Boot เป็น Disabled
  • เชื่อมต่อผ่าน Native USB Port ของ ESP32-C6 แต่เลือกใช้วิธีนี้ ให้เลือก USB CDC On Boot เป็น Enabled

ถ้าต้องการเข้าโหมด USB Bootloader เพื่ออัปโหลดไฟล์เฟิร์มแวร์ไปยังบอร์ด ให้กดปุ่ม BOOT ค้างไว้ แล้วกดปุ่ม Reset แล้วปล่อย

รูป: การเชื่อมต่อกับบอร์ดด้วย USB (Source: Espressif)

บอร์ดมีปุ่ม RESET (ขา CHIP_PU) และปุ่ม BOOT (ขา GPIO9) และมีวงจร WS2812 RGB LED (ขา GPIO8) ถ้าต้องการใช้ขาอินพุตแบบแอนะล็อก ก็มีให้เลือกใช้ 7 ช่องสัญญาณ ได้แก่ ADC1_CH0..CH6 (ตรงกับขา GPIO0..GPIO6 ตามลำดับ)

 

บอร์ด WeAct Studio ESP32-C6-A มีลักษณะคล้ายกับ ESP32-C6 DevKitC-1 ของบริษัท Espressif Systems

รูป: แผนผังแสดงองค์ประกอบสำคัญของบอร์ด WeAct Studio ESP32-C6-A

ในเชิงเปรียบเทียบความแตกต่าง บอร์ด ESP32-C6-DEV-KIT-N8 มีคอนเนกเตอร์ USB Type-C เพียงอันเดียว แต่มีการใช้งานชิป USB Hub เพื่อเชื่อมต่อและแชร์การใช้งาน USB ระหว่างไอซี USB-to-Serial Bridge และขาสัญญาณ USB D+/D- ของ ESP32-C6

รูป: แผนผังแสดงองค์ประกอบสำคัญของบอร์ด ESP32-C6-DEV-KIT-N8

รูป: แผนผังของบอร์ด ESP32-C6-DEV-KIT-N8

 


โค้ดตัวอย่างที่ 1: การตรวจสอบคุณสมบัติเกี่ยวกับฮาร์ดแวร์และซอฟต์แวร์#

โค้ดตัวอย่างแรก สาธิตการเขียนโค้ดโดยใช้ Arduino-ESP32 Core สำหรับบอร์ด ESP32-C6 และสาธิตการใช้คำสั่งของคลาส ESP เพื่อแสดงข้อมูลเกี่ยวกับฮาร์ดแวร์และซอฟต์แวร์ เช่น

  • ESP.getCoreVersion() ข้อความระบุเวอร์ชันของ Arduino ESP32 Core
  • ESP.getSdkVersion() ข้อความระบุเวอร์ชันของ ESP-IDF
  • ESP.getChipModel() ข้อความระบุโมเดลหรือชื่อรุ่นของ ESP32 Series
void showESPInfo() {  
  Serial.println( "Hardware Info..." );

  Serial.printf( "- ESP chip model   : %s\n", 
                 ESP.getChipModel() );
  Serial.printf( "- Chip revision    : %u\n", 
                 ESP.getChipRevision() );
  Serial.printf( "- No. of cores     : %u\n", 
                 ESP.getChipCores() );
  Serial.printf( "- Chip MAC address : %llx\n", 
                 ESP.getEfuseMac() );

  Serial.printf( "- Total heap size  : %lu bytes\n", 
                 ESP.getHeapSize() );
  Serial.printf( "- Free heap size   : %lu bytes\n", 
                 ESP.getFreeHeap() );
  Serial.printf( "- Total PSRAM size : %lu bytes\n", 
                 ESP.getPsramSize() );
  Serial.printf( "- Free PSRAM size  : %lu bytes\n", 
                 ESP.getFreePsram() );

  Serial.printf( "- SPI flash size   : %lu MB\n", 
                 ESP.getFlashChipSize()/(1024*1024) );
  Serial.printf( "- SPI flash speed  : %lu MHz\n\n",
                 ESP.getFlashChipSpeed()/(long)1e6 );

  Serial.println( "Software Info..." );

#if ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(3, 0, 0)
  Serial.printf( "- Arduino ESP32 Core : v%s\n", 
                 ESP.getCoreVersion() );
#else
  Serial.printf( "- Arduino ESP32 Core : v%i.%i.%i\n",
                 ESP_ARDUINO_VERSION_MAJOR, 
                 ESP_ARDUINO_VERSION_MINOR, 
                 ESP_ARDUINO_VERSION_PATCH );
#endif
  Serial.printf( "- Espressif ESP-IDF  : %s\n\n", 
                 ESP.getSdkVersion() );
}

void setup() {
  Serial.begin( 115200 );
  Serial.println("\n\n\n");
}

void loop() {
  showESPInfo();
  delay(5000);
}

ถ้าอัปโหลดเฟิร์มแวร์แล้ว มีข้อความ waiting for download ได้รับจากบอร์ดผ่านทาง USB-to-Serial ให้กดปุ่มรีเซตเพื่อให้โปรแกรมเริ่มต้นทำงาน

รูป: ตัวอย่างการเลือก Serial Port (เชื่อมต่อผ่าน USB-to-Serial Bridge) สำหรับอัปโหลดไฟล์เฟิริม์แวร์

รูป: ตัวอย่างข้อความเอาต์พุต

 


โค้ดตัวอย่างที่ 2: การตรวจสอบสถานะลอจิกของปุ่มกดและเปลี่ยนสถานะลอจิกของ LED#

ตัวอย่างถัดไปเป็นการตรวจสอบสถานะลอจิกของปุ่มกด โดยจะใช้วงจรปุ่มกดและวงจร LED ที่นำมาต่อเพิ่มที่ขา GPIO ของ ESP32-C6

ในการตรวจสอบสถานะของปุ่มกด จะใช้วิธีเปิดการใช้งานอินเทอร์รัพท์ภายนอกที่ขาอินพุตสำหรับปุ่มกด ซึ่งทำงานแบบ Active-Low เมื่อพบว่า มีการกดปุ่มแล้วปล่อย จะเปลี่ยนสถานะลอจิกที่ขาเอาต์พุตสำหรับ LED หนึ่งครั้ง

// Define the pin number for the LED.
const int LED_PIN = 10;
// Define the pin number for the button.
const int BTN_PIN = 11;

// Declare a variable to store the state of the LED.
bool led_state = 0;

void setup() {
  // Begin serial communication at a baud rate of 115200.
  Serial.begin(115200);
  // Set the LED pin as an output.
  pinMode( LED_PIN, OUTPUT );
  // // Set the button pin as an input with pull-up.
  pinMode( BTN_PIN, INPUT_PULLUP );
  // Attach an interrupt to the button pin 
  // that triggers on a rising edge.
  // Note: The ISR is implemented as an anonymous function.
  attachInterrupt( BTN_PIN, [](){
    // Use a static variable to store the time of 
    // the last state change.
    static uint32_t last_changed = 0;
    // Get the current time in msec.
    uint32_t now = millis();
    // Check if at least 10 milliseconds have passed 
    // since the last state change.
    if ( now - last_changed >= 10 ) {
      // Toggle the state of the LED.
      digitalWrite( LED_PIN, led_state ^= 1 );
    }
    // Update the time of the last state change
    last_changed = now;
  }, RISING );
}

void loop() { 
}

รูป: การต่อวงจร Button และ LED บนเบรดบอร์ด

 

ถ้าจะลองใช้ FreeRTOS เพื่อให้มีการสื่อสารจากฟังก์ชัน ISR (Interrupt Service Routine) ไปยังการทาสก์หลัก (Main Task) ซึ่งทำงานในฟังก์ชัน loop() เมื่อเกิดอินพุตจากการกดปุ่ม ก็มีแนวทางดังนี้

const int LED_PIN = 10;
const int BTN_PIN = 11;

bool led_state = 0;
TaskHandle_t mainTaskHandle = NULL;

void notifyChange() {
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;
  // Notify the main task.
  vTaskNotifyGiveFromISR(mainTaskHandle, &xHigherPriorityTaskWoken);
  if (xHigherPriorityTaskWoken == pdTRUE) {
    portYIELD_FROM_ISR();
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  pinMode(BTN_PIN, INPUT_PULLUP);
  // Get the handle of the main task.
  mainTaskHandle = xTaskGetCurrentTaskHandle();
  // Attach the interrupt to the button pin
  attachInterrupt( BTN_PIN, [](){
    static uint32_t last_changed = 0;
    uint32_t now = millis();
    if ( now - last_changed >= 10 ) {
      // Notify the main task.
      notifyChange();
    }
    last_changed = now;
  }, RISING);
}

void loop() {
  // A static variable used to count the button clicks.
  static uint32_t clicks = 0;
  // Wait for task notification from the ISR with timeout.
  if ( ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(10))==pdTRUE) {
    // Toggle the LED state.
    digitalWrite(LED_PIN, led_state ^= 1);
    Serial.printf("LED state: %d, clicks: %lu\n", 
                  led_state, ++clicks);
  }
}

 

การสื่อสารระหว่าง ISR และทาสก์หลัก ยังมีวิธีอื่นอีกเมื่อใช้งาน FreeRTOS เช่น การใช้ Queue ตามตัวอย่างต่อไปนี้

const int LED_PIN = 10;
const int BTN_PIN = 11;

bool led_state = 0;
TaskHandle_t mainTaskHandle = NULL;
QueueHandle_t eventQueue;

void notifyChange() {
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;
  // Notify the main task
  xQueueSendToBackFromISR(eventQueue, &led_state,
                          &xHigherPriorityTaskWoken);
  if (xHigherPriorityTaskWoken == pdTRUE) {
    portYIELD_FROM_ISR();
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  pinMode(BTN_PIN, INPUT_PULLUP);
  // Get the handle of the main task.
  mainTaskHandle = xTaskGetCurrentTaskHandle();
  // Create an event queue with a capacity of 1.
  eventQueue = xQueueCreate(1, sizeof(bool));
  // Attach the interrupt to the button pin.
  attachInterrupt( BTN_PIN, []() {
    static uint32_t last_changed = 0;
    uint32_t now = millis();
    if ( now - last_changed >= 10 ) {
      // Notify the main task.
      notifyChange();
    }
    last_changed = now;
  }, RISING);
}

void loop() {
  // A static variable used to count the button clicks.
  static uint32_t clicks = 0;
  // Wait for an event in the event queue (with a timeout).
  bool event;
  if (xQueueReceive(eventQueue,&event,pdMS_TO_TICKS(10))==pdTRUE){
    // Toggle the LED state.
    digitalWrite(LED_PIN, event ^= 1);
    Serial.printf("LED state: %d, clicks: %lu\n", 
                  event, ++clicks);
  }  
}

 


โค้ดตัวอย่างที่ 3: การปรับความสว่างของ LED ด้วยสัญญาณ PWM#

ตัวอย่างถัดไปสาธิตการใช้คำสั่งเพื่อปรับความสว่างของ LED โดยการสร้างสัญญาณ PWM ที่สามารถปรับค่า Duty Cycle ของสัญญาณได้ และใช้วงจร LEDC / Timer ภายในชิป ESP32C6 เพื่อสร้างสัญญาณดังกล่าว คำสั่งที่เกี่ยวข้องได้แก่

  • analogWriteResolution(uint8_t pin, uint8_t resolution)
  • analogWriteFrequency(uint8_t pin, uint32_t freq)
  • analogWrite(uint8_t pin, int value)

ในโค้ดตัวอย่างนี้ ได้กำหนดค่าความถี่ของสัญญาณ PWM ไว้เท่ากับ 1000Hz และความละเอียดในการกำหนดค่าให้สัญญาณ PWM ไว้ที่ 8 บิต (สูงสุด)

const uint8_t LED_PIN  = 10;
const uint8_t PWM_BITS = 8;
const uint32_t PWM_MAX_VALUE = ((1<<PWM_BITS)-1);
const uint32_t PWM_FREQ = 1000;

void setup() {
  Serial.begin(115200);
  Serial.println("PWM LED Dimming");
  pinMode(LED_PIN, OUTPUT);
  analogWriteResolution( LED_PIN, PWM_BITS );
  analogWriteFrequency( LED_PIN, PWM_FREQ /*Hz*/ );
  analogWrite( LED_PIN, 0 );
}

void loop() {
  int value;
  for (int i=0; i < (2*PWM_MAX_VALUE+1); i++){
    value = (i >= PWM_MAX_VALUE) ? (2*PWM_MAX_VALUE-i) : i;
    // Write the new PWM value.
    analogWrite( LED_PIN, value );
    Serial.printf( "PWM:%d\n", value );
    delay(8);
  }
}

การสร้างสัญญาณ PWM ถ้าไม่ใช้คำสั่ง analogWrite() ตามรูปแบบของ Arduino API ก็มีคำสั่งอื่นให้ใช้งานได้เช่นกัน คำสั่งที่เกี่ยวข้องได้แก่

  • ledcAttach(uint8_t pin, uint32_t freq, uint8_t resolution)
  • ledcWrite(uint8_t pin, uint32_t duty)
  • ledcRead(uint8_t pin)

ในโค้ดตัวอย่างนี้ ได้กำหนดค่าความถี่ของสัญญาณ PWM ไว้เท่ากับ 1000Hz และความละเอียดในการกำหนดค่าให้สัญญาณ PWM ไว้ที่ 10 บิต

const uint8_t LED_PIN  = 10;
const uint8_t PWM_BITS = 10;
const uint32_t PWM_MAX_VALUE = ((1<<PWM_BITS)-1);
const uint32_t PWM_FREQ = 1000;

void initLEDC(void) {
  if (!ledcAttach(LED_PIN, PWM_FREQ, PWM_BITS)) {
    Serial.println( "LEDC initialization failed!" );
  } else {
    ledcWrite( LED_PIN, 0 ); 
  }
}

void setup() {
  Serial.begin(115200);
  Serial.println("PWM LED Dimming");
  pinMode(LED_PIN, OUTPUT);
  initLEDC();
}

void loop() {
  int value;
  for ( int i=0; i < (2*PWM_MAX_VALUE+1); i++ ) {
    value = (i >= PWM_MAX_VALUE) ? (2*PWM_MAX_VALUE-i) : i;
    // Write the new PWM value.
    ledcWrite( LED_PIN, value ); 
    delay(2);
    Serial.printf( "PWM:%d (%d) \n", value, 
                   (int)ledcRead(LED_PIN) );
  }
}

 

ถ้าต้องการลองใช้คำสั่งของ ESP-IDF v5.1 ในกลุ่มคำสั่ง LEDC API เช่น

  • ledc_timer_config( ... ) ตั้งค่าสำหรับวงจรตัวนับเพื่อใช้งานกับ LEDC ความละเอียดในการนับ (จำนวนบิต) ความถี่ และแหล่งที่มาของสัญญาณ Clock
  • ledc_channel_config( ... ) ตั้งค่าเลือกช่องสัญญาณของวงจร LEDC
  • ledc_fade_func_install( ... ) เปิดการใช้งานโหมด Duty Cycle Fading เพื่อให้ปรับเพิ่มหรือลดค่า Duty Cycle ได้แบบอัตโนมัติ
  • ledc_cb_register( ... ) กำหนดฟังก์ชัน Callback เมื่อจบการทำงานของ Fading Function
  • ledc_set_fade_with_time( ... ) กำหนดระยะเวลาในการทำงานของ Fading Function
  • ledc_fade_start( ... ) เริ่มต้นการทำงานของ Fading Function
  • ledc_set_duty( ... ) กำหนดค่า PWM Duty Cycle
  • ledc_update_duty( ... ) อัปเดตสัญญาณ PWM
  • ledc_get_duty( ... ) อ่านค่าของ PWM Duty Cycle

ก็มีตัวอย่างดังนี้

#include "driver/ledc.h"
#include "hal/ledc_types.h"

#define LEDC_GPIO      (10)
#define LEDC_TIMER     (LEDC_TIMER_1)
#define LEDC_MODE      (LEDC_LOW_SPEED_MODE)
#define LEDC_CHANNEL   (LEDC_CHANNEL_2)

#define PWM_BITS       (LEDC_TIMER_10_BIT)
#define PWM_MAX_VALUE  ((1<<PWM_BITS)-1)
#define PWM_FREQ       (1000)

// This callback function will be called 
// when fade operation has ended.
static IRAM_ATTR bool cb_ledc_fade_end_event(
  const ledc_cb_param_t *param, void *user_arg )
{
  if (param->event == LEDC_FADE_END_EVT) {
    // Handle the event if necessary...
  }
  return false;
}

ledc_channel_config_t ledc_channel;
ledc_channel_config_t *ledc = NULL;

void initLEDC(void) {
  ledc_timer_config_t _ledc_timer = {
     .speed_mode = LEDC_MODE,     // low-speed timer mode 
     .duty_resolution = PWM_BITS, // resolution of PWM duty
     .timer_num = LEDC_TIMER,     // timer index
     .freq_hz = 1000,             // PWM frequency in Hz
     .clk_cfg = LEDC_AUTO_CLK,    // Auto select the source clk
  };
  // Set configuration of timer0 for high speed channels
  ledc_timer_config(&_ledc_timer);

  ledc_channel_config_t _ledc_channel = {
    .gpio_num   = LEDC_GPIO,
    .speed_mode = LEDC_MODE,
    .channel    = LEDC_CHANNEL,
    .intr_type  = LEDC_INTR_FADE_END,
    .timer_sel  = LEDC_TIMER,
    .duty       = 0,
    .hpoint     = 0,
    .flags = { .output_invert = 1 },
  };

  ledc_channel = _ledc_channel;
  ledc_channel_config(&ledc_channel);
  ledc = &ledc_channel;

  // Initialize the LEDC fade service.
  ledc_fade_func_install(0);
  ledc_cbs_t callbacks = {
    .fade_cb = cb_ledc_fade_end_event
  };
  ledc_cb_register( ledc->speed_mode, ledc->channel, 
                    &callbacks, (void *)NULL );
}

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

void demo1() {
  Serial.printf("LEDC: fade up to %d.\n", PWM_MAX_VALUE );
  ledc_set_fade_with_time(
    ledc->speed_mode, ledc->channel, PWM_MAX_VALUE, 2000 
  );
  ledc_fade_start(
    ledc->speed_mode, ledc->channel, LEDC_FADE_NO_WAIT
  );

  Serial.printf("LEDC: fade down to 0.\n");
  ledc_set_fade_with_time(
    ledc->speed_mode, ledc->channel, 0, 2000
  );
  ledc_fade_start(
    ledc->speed_mode, ledc->channel, LEDC_FADE_NO_WAIT
  );
}

void demo2(){
  int value;
  Serial.printf("LEDC: set the duty cycle.\n" );
  for ( int i=0; i < (2*PWM_MAX_VALUE+1); i++ ) {
    value = (i >= PWM_MAX_VALUE) ? (2*PWM_MAX_VALUE-i) : i;
    ledc_set_duty(ledc->speed_mode, ledc->channel, value);
    ledc_update_duty(ledc->speed_mode, ledc->channel);
    delay(2);
    value = ledc_get_duty(ledc->speed_mode, ledc->channel);
    Serial.printf( "PWM:%d\n", value );
  }
}

void loop() {
  demo1(); delay(2000);
  demo2(); delay(2000);
}

 


โค้ดตัวอย่างที่ 4: การสื่อสารด้วย I2C Master#

ตัวอย่างถัดไป สาธิตการใช้งาน I2C ของ ESP32-C6 ในโหมด I2C Master โดยเลือกใช้ขา GPIO6 และ GPIO7 สำหรับสัญญาณ I2C SDA และ I2C SCL ตามลำดับ อุปกรณ์ที่นำมาต่อใช้งานคือ โมดูลเซนเซอร์ AHT2x (Temperature & Humdity Sensor) ซึ่งมีหมายเลขแอดเดรส 0x38 โค้ดตัวอย่างสาธิตการตรวจสอบอุปกรณ์ที่เชื่อมต่อกับบัส I2C

#include "Wire.h"

#define I2C_SDA_PIN   (6)
#define I2C_SCL_PIN   (7)
#define INTERVAL_MSEC (4000)

void i2c_scan();

void setup() {
  Serial.begin(115200);
  Serial.println("\n\n\n");
  // Initialize the I2C bus as master.
  Wire.begin( I2C_SDA_PIN, I2C_SCL_PIN );
}

void loop() {
  static uint32_t ts = 0;
  uint32_t now = millis();
  if ( now - ts >= INTERVAL_MSEC ) {    
    ts = now;
    i2c_scan(); // Scan I2C slave devices.
  }
}

#define LINE_SEP  "------------------"

void i2c_scan() {
  char sbuf[32];
  int n_devices = 0;
  Serial.println( "Scanning I2C bus..." );
  Serial.print( "   " );
  for ( uint8_t col=0; col < 16; col++ ) {
    sprintf( sbuf, "%3x", col );
    Serial.print( sbuf );
  }
  Serial.println( "" );
  uint8_t addr=0;
  for( uint8_t row=0; row < 8; row++ ) {
    sprintf( sbuf, "%02x:", row << 4 );
    Serial.print( sbuf );
    for ( uint8_t col=0; col < 16; col++ ) {
      if ( row==0 && addr<=1 ) {
        Serial.print("   ");
      } else {
        Wire.beginTransmission( addr );
        if ( Wire.endTransmission() > 0 ) {
          Serial.print( " --" );
        } else {
          sprintf( sbuf, " %2x", addr );
          Serial.print( sbuf );
          n_devices++;
        }
      }
      addr++;
    }
    Serial.println( "" );
  } // end for
  Serial.println( LINE_SEP LINE_SEP LINE_SEP );
  Serial.flush();
}

รูป: ตัวอย่างข้อความเอาต์พุต

ถ้าจะลองใช้คำสั่งตามรูปแบบของ ESP-IDF สำหรับ I2C Driver ให้ศึกษาได้จาก ESP32-C6 Peripherals API for I2C

 


โค้ดตัวอย่างที่ 5: การอ่านค่าจากโมดูลเซนเซอร์ AHT2x#

โค้ดตัวอย่างถัดไปสาธิตการติดตั้งและใช้งานไลบรารี เพื่ออ่านค่าจากโมดูล AHT2x ที่เชื่อมต่อด้วยบัส I2C โดยได้เลือกใช้ไลบรารี Adafruit_AHTX0 ของบริษัท Adafruit

#include <Adafruit_AHTX0.h>

#define I2C_SDA_PIN   (6)
#define I2C_SCL_PIN   (7)
#define INTERVAL_MSEC (2000)

// Create an instance of the Adafruit_AHTX0 class.
Adafruit_AHTX0 aht;

void setup() {
  Serial.begin(115200);
  Serial.println("\n\n\n");
  // Initialize the I2C bus as a master.
  Wire.begin( I2C_SDA_PIN, I2C_SCL_PIN, 400000 /*speed*/ );
  Serial.println( "Adafruit AHT10/AHT20 library demo!" );
  if (!aht.begin()) {
    Serial.println("AHT2x initialization failed...");
    while (1) delay(10);
  }
  Serial.println("Start reading the AHT2x sensor...");
}

void loop() {
  static uint32_t ts;
  uint32_t now = millis();
  if ( now - ts >= INTERVAL_MSEC ) {    
    ts = now;
    sensors_event_t humidity, temp;
    if ( aht.getEvent(&humidity, &temp) ) {
      Serial.printf( "T: %.1f deg.C, H: %.1f %%RH\n", 
                (float)temp.temperature,
                (float)humidity.relative_humidity );
    } else { // error
      Serial.printf( "AHT2x reading error!\n" );
    }
  }
}

รูป: ตัวอย่างการติดตั้งไลบรารี Adafruit_AHTX0

รูป: ตัวอย่างข้อความเอาต์พุต

รูป: ตัวอย่างการต่อวงจรเพื่อใช้งานโมดูล AHT21

 


โค้ดตัวอย่างที่ 6: การกำหนดค่าสีของโมดูล WS2812 RGB LED#

ตัวอย่างถัดไปสาธิตการเขียนโค้ดเพื่อกำหนดค่าสีให้กับ WS2812 RGB LED (จำนวน 1 ดวง) ที่ต่อกับขา GPIO8 และมีการใช้งานปุ่มกด BOOT (ขา GPIO9) เพื่อเปลี่ยนสีของ RGB ตามค่าสี (24 บิต) ที่ได้มีการกำหนดไว้ในอาร์เรย์

การกำหนดค่าสี 24 บิต ต่อหนึ่งพิกเซล จะต้องมีการสร้างสัญญาณพัลส์ โดยใช้วงจร RMT และความกว้างของพัลส์ช่วงที่เป็น High และ Low จะเป็นตัวกำหนดค่าบิตเป็น '0' หรือ '1' ตามพารามิเตอร์เชิงเวลาของบิตต่อไปนี้

  • T0H = '0' bit high time
  • T0L = '0' bit low time
  • T1H = '1' bit high time
  • T1L = '1' bit low time

โมเดล WS2812 และ WS2812B มีค่าพารามิเตอร์เชิงเวลาแตกต่างกัน (และมีค่าความคลาดเคลื่อนได้ในช่วงที่กำหนดไว้ตามเอกสารของผู้ผลิต)

  • WS2812 (1.25us bit time, 800Kbps)
    • '0' bit: T0H = 0.35us, T1L = 0.8us
    • '1' bit: T1H = 0.70us, T1L = 0.6us
  • WS2812b: (1.25us bit time, 800Kbps)
    • '0' bit: T0H = 0.40us, T0L = 0.85us
    • '1' bit: T1H = 0.80us, T1L = 0.45us

คำสั่งที่เกี่ยวข้องกับการใช้งานวงจร RMT สำหรับการสร้างสัญญาณเอาต์พุตให้กับ WS2812 ได้แก่

  • rmtInit()
  • rmtWrite()
const int BTN_PIN = 9;    // onboard BOOT button 
const int WS2812_PIN = 8; // onboard RGB LED (single pixel)

const uint32_t NUM_RGB_BITS = 24;  // 24 bits per RGB LED
rmt_data_t led_data[NUM_RGB_BITS];

const uint32_t COLORS [] = {
   0x00001f, // Greeen
   0x001f00, // Red
   0x1f0000, // Blue
   0x000000  // OFF
};
const uint32_t NUM_COLORS = sizeof(COLORS)/sizeof(uint32_t);

// FreeRTOS task handle
TaskHandle_t buttonTask;

void setColor( uint32_t color ) {
  for (uint32_t i=0; i < 24; i++) {
    if ( (color >> (24-i-1)) & 1 ) {
      led_data[i].level0 = 1; // T1H
      led_data[i].duration0 = 8;
      led_data[i].level1 = 0; // T1L
      led_data[i].duration1 = 4;
    } else {
      led_data[i].level0 = 1; // T0H
      led_data[i].duration0 = 4;
      led_data[i].level1 = 0; // T0L
      led_data[i].duration1 = 8;
    }
  }
  rmtWrite(WS2812_PIN, led_data, NUM_RGB_BITS, RMT_WAIT_FOR_EVER);
}

// Function to handle button press
void buttonAction(void *pvParameters) {
  (void)pvParameters;
  uint32_t index = NUM_COLORS-1;
  while (1) {
    // Wait for task notification.
    ulTaskNotifyTake( pdTRUE, portMAX_DELAY );
    index = (index+1) %  NUM_COLORS;
    Serial.printf( "Button pressed! (#%lu)\n", index );
    setColor( COLORS[index] );
  }
}

void setup() {
  Serial.begin( 115200 );
  Serial.println( "ESP32-C6 FreeRTOS Demo..." );
  pinMode( BTN_PIN, INPUT_PULLUP );

  // Initialize the RMT TX driver.
  if (!rmtInit(WS2812_PIN, RMT_TX_MODE, RMT_MEM_NUM_BLOCKS_1, 10000000)) {
    Serial.println("RMT TX initialization failed\n");
  }
  setColor(0x7f7f7f);
  delay(500);
  setColor(0);

  // Attach an external interrupt to the button pin.
  attachInterrupt( digitalPinToInterrupt(BTN_PIN), [] {
    static uint32_t last_action = 0;
    uint32_t now = millis();
    if ( now - last_action >= 10 && digitalRead(BTN_PIN)==HIGH ) {
      last_action = now;
      // Send a task notification to the button task.
      BaseType_t xHigherPriorityTaskWoken = pdFALSE;
      vTaskNotifyGiveFromISR( buttonTask, &xHigherPriorityTaskWoken );
      if (xHigherPriorityTaskWoken==pdTRUE) {
        portYIELD_FROM_ISR();
      }
    }
  }, RISING);

  // Create the button task.
  xTaskCreate(
     buttonAction,   // Task function
     "ButtonTask",   // Task name
     2048,           // Stack size (words)
     NULL,           // Task input parameter
     1,              // Task priority
     &buttonTask     // Task handle
  );
}

void loop() {
}

ดูตัวอย่างการเขียนโค้ดเพื่อใช้งาน RMT ได้จาก

https://github.com/espressif/arduino-esp32/.../libraries/ESP32/examples/RMT/

 


โค้ดตัวอย่างที่ 7: การเชื่อมต่อผ่าน WiFi ไปยัง NTP Server#

ตัวอย่างถัดไปเป็นการเขียนโค้ดเพื่อเชื่อมต่อผ่าน WiFi เพื่อให้ ESP32C6 ปรับเวลาของระบบ RTC ให้ตรงกับเวลาของ NTP Server โดยได้ทดลองใช้ไลบรารีที่มีชื่อว่า NTPClient.h

#include <WiFi.h>
#include <WiFiUdp.h>
#include <NTPClient.h> // "NTPClient by Fabrice Weinberg"
#include "Secret.h"

const char *DAYS_OF_WEEK[7]={
   "Sunday", "Monday", "Tuesday", "Wednesday", 
   "Thursday", "Friday", "Saturday"
};
const char *MONTHS[12]={
  "January", "February", "March", "April", "May", "June", 
  "July", "August", "September", "October", "November", "December"
};

// NTP Servers:
const char* NTP_SERVER = "th.pool.ntp.org";
const int   NTP_PORT = 123;

WiFiUDP wifiUDP; // Use a UDP client  to connect to the NTP server.
NTPClient timeClient( wifiUDP, NTP_SERVER, NTP_PORT );

void connectWifi() {
  WiFi.disconnect();
  WiFi.mode(WIFI_OFF);
  delay(10);
  WiFi.mode(WIFI_STA);
  WiFi.setTxPower(WIFI_POWER_19_5dBm);
  // Connect to Wi-Fi
  WiFi.begin(WIFI_SSID, WIFI_PASSWD);
  Serial.println("Connecting to WiFi...");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(500);
  }
  Serial.println("\nConnected to WiFi");
}

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

  // Connect to the NTP Server.
  timeClient.begin();
  // synchronize with the NTP server to update the date and time.
  if (timeClient.update()) { // OK
    // Time synchronization successful
    Serial.println("Time synchronized: " + timeClient.getFormattedTime());
    timeClient.setTimeOffset( 7*60*60 /*sec*/ ); // UTC+7 (Bangkok)
    Serial.println( timeClient.getFormattedTime() );
  } 
  else {
    // Time synchronization failed.
    Serial.println("Cannot synchronize with NTP!");
  }
}

void showDateTime() {
  //Serial.println( timeClient.getFormattedTime() );
  time_t epochTime = timeClient.getEpochTime();
  Serial.printf( "Epoch time since Jan 1, 1970: %llu sec\n", epochTime );

  struct tm timeInfo;
  localtime_r(&epochTime, &timeInfo);
  // Extract the month and year
  int dayOfMonth = timeInfo.tm_mday; // Day of the month (1-31)
  int month = timeInfo.tm_mon ;      // Month (0-11)
  int year  = timeInfo.tm_year + 1900; // Year (years since 1900)
  // The week day (0 to 6) starting on Sunday
  int wday  = timeClient.getDay(); 
  String dateString;
  dateString += DAYS_OF_WEEK[wday];
  dateString += " ";
  dateString += String(MONTHS[month]);
  dateString += " ";
  dateString +=  String(dayOfMonth);
  dateString += ", ";
  dateString += String(year);
  dateString += " ";
  Serial.print( dateString );
  int hh = timeClient.getHours(); // (0 to 23) in 24 hour format
  int mm = timeClient.getMinutes(); // (0 to 59)
  int ss = timeClient.getSeconds(); // (0 t0 59)
  Serial.printf("%02d:%02d:%02d\n\n", hh, mm, ss );
}

void loop() {
  static uint32_t ts = 0;
  uint32_t now = millis();
  if (now - ts >= 1000) {
    ts = now;
    showDateTime();
  }
}

การตั้งค่าชื่อ SSID และรหัสผ่าน จะอยู่ในไฟล์ Secret.h

File: Secret.h

const char* WIFI_SSID   = "YOUR_WIFI_SSID";
const char* WIFI_PASSWD = "YOUR_WIFI_PASSWORD";

 


โค้ดตัวอย่างที่ 8: การอ่านค่าสัญญาณแอนะล็อกด้วย ADC#

คำสั่งที่เกี่ยวข้องกับ ADC (ดูได้จาก esp32-hal-adc.h) เช่น

  • analogReadResolution( ... ) กำหนดความละเอียดของข้อมูล (จำนวนบิต) ที่ได้จาก ADC
  • analogSetPinAttenuation( ... ) กำหนดอัตราการลดทอนสัญญาณอินพุตของแต่ละช่องอินพุต
  • analogReadMilliVolts( ... ) อ่านค่าจาก ADC ซึ่งจะได้ค่าตัวเลขในหน่วยเป็นมิลลิโวลต์
const int ADC_PIN = 4;  // ADC1_CH4 / GPIO4 pin

// Initialize the ADC input channel.
void initADC() { 
  // Set ADC resolution to 12 bits
  analogReadResolution( 12 );
  // Set attenuation level to 11 dB.
  analogSetPinAttenuation( ADC_PIN, ADC_11db ); 
}

void setup() {
  Serial.begin(115200);
  initADC(); // Initialize the ADC.
 }

void loop() {
  uint16_t value = (uint16_t)analogReadMilliVolts( ADC_PIN );
  Serial.printf("S:%u,MIN:0,MAX:3300\n", value );
  delay(100);
}

รูป: ตัวอย่างการแสดงข้อมูลที่ได้รับใน Arduino Serial Plotter เมื่ออ่านค่าสัญญาณแอนะล็อกจากโมดูลเซนเซอร์แสง

 


โค้ดตัวอย่างที่ 9: การอ่านค่าสัญญาณแอนะล็อกด้วยอัตราคงที่โดยใช้ ADC และ Timer#

ตัวอย่างโค้ดถัดไปเป็นการเขียนโค้ดเพื่ออ่านค่าสัญญาณอินพุตแบบแอนะล็อก โดยใช้หนึ่งช่องสัญญาณอินพุต ของวงจร ADC (Analog-to-Digital Converter) ภายในชิป ESP32C6 (เลือกใช้ขา ADC1_CH4 / GPIO4)

การอ่านค่าอินพุต จะใช้วงจร Hardware Timer เป็นตัวกำหนดอัตราการอ่านให้คงที่ โดยกำหนดไว้ 10000Hz ค่าที่อ่านได้ในแต่ละครั้งจะได้ค่าเป็นเลขจำนวนเต็มหน่วยเป็นมิลลิโวลต์ ไม่เกิน 3300mV และจะบันทึกข้อมูลลงในอาร์เรย์ขนาด 1024 เมื่อได้ข้อมูลครบแล้ว จะส่งข้อมูลออกทาง Serial (ตั้งค่า Baudrate 921600) เป็นข้อความ หนึ่งค่าตัวเลขต่อหนึ่งบรรทัด ไปยังคอมพิวเตอร์ แล้วเริ่มต้นขั้นตอนซ้ำใหม่ในรอบถัดไป

const int ADC_PIN = 4;   // ADC1_CH4 / GPIO4 pin

bool sampling = true;
uint32_t sample_index = 0;

const uint32_t Fs = 10000; // Sampling frequency (Hz)
const uint32_t N  = 1024;  // Number of samples
uint32_t sample_count = 0;
uint16_t samples[N];

TaskHandle_t mainTaskHandle = NULL;

void notifyTask() {
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;
  // Notify the main task.
  vTaskNotifyGiveFromISR(mainTaskHandle, &xHigherPriorityTaskWoken);
  if (xHigherPriorityTaskWoken == pdTRUE) {
    portYIELD_FROM_ISR();
  }
}

//----------------------------------------------------------------
// Callback function of the hardware timer.
void IRAM_ATTR timer_callback() {
  if (!sampling)
      return;
  // Read the ADC input channel.
  uint16_t value = (uint16_t)analogReadMilliVolts( ADC_PIN );
  samples[sample_count++] = value;
  if ( sample_count == N ) {
    sampling = false;  // Pause the ADC reading.
    sample_count = 0;  // Reset the sample count.
    notifyTask();      // Notify the main task.
  }
}

// Initialize the ADC input channel.
void initADC() { 
  // Set ADC resolution to 12 bits
  analogReadResolution( 12 );
  // Set attenuation level to 11 dB.
  analogSetPinAttenuation( ADC_PIN, ADC_11db ); 
}

// Initialize the hardware timer.
void initTimer( uint32_t hw_timer_unit=0 ) {
  static hw_timer_t *timer = NULL;
  timer = timerBegin( 1000000UL ); // 1MHz (1us tick)
  timerWrite(timer, 0);
  // Attach the callback function (ISR) to the timer
  timerAttachInterrupt( timer, &timer_callback );
  timerAlarm(timer, (1000000UL/Fs) /*interval*/,
             true /*reload*/, 0 /*reload value*/);
  timerRestart(timer);
}

void setup() {
  Serial.begin(921600);
  Serial.setTxBufferSize(256);
  Serial.flush();
  // Get the handle of the main task.
  mainTaskHandle = xTaskGetCurrentTaskHandle();  
  initADC();   // Initialize the ADC.
  initTimer(); // Initialize the hardware timer.
}

void loop() {
  if ( ulTaskNotifyTake(pdTRUE,pdMS_TO_TICKS(5))==pdTRUE) {
    for ( uint32_t i=0; i < N; i++ ) {
      // Send the sample as a string to serial.
      Serial.printf("S:%u,MIN:0,MAX:3300\n", samples[i]);
    }
    Serial.flush();
    sampling = true;
  }
}

สัญญาณอินพุตแบบแอนะล็อก อาจได้จากเครื่องกำเนิดสัญญาณ (Function Generator) หรือโมดูลเซนเซอร์ที่ให้เอาต์พุตเป็นสัญญาณแอนะล็อก เช่น โมดูลไมโครโฟนขยายเสียง MAX4466 หรือ MAX9814

รูป: ตัวอย่างการแสดงข้อมูลที่ได้รับใน Arduino Serial Plotter เมื่ออ่านค่าสัญญาณแอนะล็อกจากโมดูลไมโครโฟนขยายเสียง

 


กล่าวสรุป#

บทความนี้ได้นำเสนอการใช้งานบอร์ดไมโครคอนโทรลเลอร์ ESP32-C6 และตัวอย่างการเขียนโค้ดด้วย Arduino Sketch และใช้ Arduino-ESP32 Core v3.0.0 เพื่อทดสอบการทำงานของฮาร์ดแวร์และซอฟต์แวร์ในเบื้องต้น

บทความที่เกี่ยวข้อง

 


This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

Created: 2023-11-18 | Last Updated: 2023-11-21