การอ่านค่าสัญญาณเสียงแอนะล็อกด้วย ESP32 และแสดงผลด้วย Python#


การวัดสัญญาณแอนะล็อกจากโมดูลไมโครโฟนเสียง#

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

บทความที่เกี่ยวข้องคือ "การใช้งานโมดูล MAX4466 Sound Sensor" ซึ่งกล่าวถึง โมดูลไมโครโฟนเสียงแบบ Electret Microphone และมีวงจรขยายเสียงที่ใช้ไอซี MAX4466 ซึ่งให้เอาต์พุตเป็นสัญญาณแอนะล็อก สัญญาณเสียงที่ถูกแปลงเป็นสัญญาณไฟฟ้าแบบแอนะล็อก จะถูกแปลงเป็นข้อมูลแบบดิจิทัลได้ โดยการใช้วงจร ADC (Analog-to-Digital Converter) อาจเป็นไอซีภายนอก หรือ วงจรภายในชิปไมโครคอนโทรลเลอร์ เช่น ESP32

ในบทความนี้ สาธิตการเขียนโค้ด Arduino-ESP32 (v3.0.0) เพื่อใช้งานวงจรภายใน ADC จำนวน 1 ช่องสัญญาณ และส่งข้อมูลที่ได้ไปยังคอมพิวเตอร์ผู้ใช้ ผ่านทาง Serial Port และมีการเขียนโค้ด Python เพื่อรับค่าและนำไปประมวลผลเชิงตัวเลข เช่น การวิเคราะห์สัญญาณแบบฟูเรียร์ ด้วย FFT (Fast-Fourier Transform)

 


โค้ด Arduino Sketch สำหรับ ESP32#

โค้ดต่อไปนี้ สาธิตการอ่านค่าจาก ADC ที่ขา GPIO34 โดยใช้วงจร Hardware Timer ทำหน้าที่กำหนดอัตราการอ่านค่าจาก ADC โดยเรียกใช้ฟังก์ชัน timer_callback()

ในโค้ดตัวอย่าง ได้กำหนดอัตราการชักตัวอย่างจากสัญญาณแอนะล็อกไว้ที่ความเร็ว Fs = 10000 ตัวอย่างต่อวินาที การอ่านข้อมูลจากอินพุตและเก็บข้อมูลลงในอาร์เรย์ samples[] จะเริ่มต้นหลังจากได้รับข้อความหนึ่งบรรทัด (จบท้ายด้วย \n) จากพอร์ต Serial และทำต่อเนื่องจนได้ข้อมูลครบ N = 1024 (เลือกจำนวนค่าตัวเลขให้เหมาะกับการนำไปคำนวณด้วย FFT)

เมื่อได้ข้อมูลครบแล้ว ในขั้นตอนถัดไป ข้อมูลแต่ละตัวในอาร์เรย์ จะถูกส่งเป็นข้อความทีละบรรทัดผ่าน Serial ด้วยความเร็ว Baudrate = 921600 จนครบ แล้วจึงเริ่มการอ่านค่า ADC และเก็บข้อมูลลงในอาร์เรย์ในรอบถัดไป

const int ADC_PIN = 34;   // ADC1_CH6 / GPIO34 pin
const int LED_PIN = 22;   // LED pin

uint32_t sample_index = 0;
bool sampling = false;
QueueHandle_t adc_queue;

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];

// Callback function of the hardware timer.
void IRAM_ATTR timer_callback() {
  if (!sampling)
      return;
  digitalWrite( LED_PIN, HIGH );
  // 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.
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;  
    xQueueSendFromISR( adc_queue, &sampling,
                       &xHigherPriorityTaskWoken );
    if (xHigherPriorityTaskWoken == pdTRUE) {
      portYIELD_FROM_ISR();
    }
  }
  digitalWrite( LED_PIN, LOW );
}

// Initialize the ADC input channel.
void initADC() { 
  // Set ADC resolution to 12 bits
  analogSetWidth( 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(1024);
  Serial.flush();
  pinMode( LED_PIN, OUTPUT );
  digitalWrite( LED_PIN, LOW );
  adc_queue = xQueueCreate(1, sizeof(uint32_t));
  initADC();   // Initialize the ADC in one-shot mode.
  initTimer(); // Initialize the hardware timer.
 }

void loop() {
  uint32_t flag;
  while (Serial.available()) {
    if (Serial.read() == '\n') {
       sampling = true; // Start the input sampling.
    }
  }
  if (xQueueReceive(adc_queue, &flag, pdMS_TO_TICKS(5))) {
    Serial.println("+++"); // Used as the frame-start symbol
    Serial.flush();
    for ( uint32_t i=0; i < N; i++ ) {
      // Send the sample as a string to serial.
      Serial.printf("%u\n", samples[i] );
    }
    Serial.flush();
    sampling = true;
  }
}

รูป: การต่อวงจรทดลองโดยใช้บอร์ด ESP32 และโมดูล MAX4466 (ใช้แรงดันไฟเลี้ยง +3.3V)

 


โค้ด Python สำหรับรับข้อมูลและวาดกราฟด้วยคอมพิวเตอร์#

ตัวอย่างถัดไปเป็นโค้ด Python สาธิตการรับข้อความจากพอร์ต Serial โดยใช้แพ็คเกจ PySerial แล้วนำมาแสดงผลในรูปของกราฟแบบ Time Series และแสดงสเปกตรัมความถี่สำหรับขนาด (Magnitude Spectrum) เมื่อนำไปผ่านการแปลงข้อมูลด้วยวิธี FFT โดยใช้แพ็คเกจ NumPy / Matplotlib

จำนวนข้อมูลที่อ่านจากพอร์ต Serial ในแต่ละรอบ จะเท่ากับขนาดของอาร์เรย์ samples[] ในโค้ด Arduino Sketch

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import signal
import serial

# Initialize serial port 
serial_port = 'COM13' # 'COMx' (Windows) or '/dev/ttyUSBx' (Linux)
ser = serial.Serial(serial_port, baudrate=921600, timeout=0.04)
ser.write(b'\r\n') # Send a newline char to start the sampling
ser.flush()

N_SAMPLES   = 1024  # Number of samples
SAMPLE_RATE = 10000 # Set sampling rate.
# Aliases
N  = N_SAMPLES
Fs = SAMPLE_RATE 

# Initialize plots
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 6))

# Plot for samples
ts = np.arange(0, N/Fs, 1/Fs)
data1, = ax1.plot(ts, np.zeros(N))
ax1.set_title('Samples')
ax1.set_xlabel('Time [s]')
ax1.set_ylabel('Amplitude')
ax1.set_xlim(ts[0], ts[-1])
amplitude_limits = [0, 3300]
ax1.set_ylim(amplitude_limits)
ax1.grid(False)

# Plot for FFT-based frequency spectrum
freq_steps = np.fft.fftfreq(N, d=1/Fs)
data2, = ax2.plot(freq_steps[:N//2], np.zeros(N//2) )
ax2.set_title("FFT-based Frequency Spectrum")
ax2.set_xlabel(f'Frequency (Hz): Fs={int(SAMPLE_RATE/1e3)} kHz')
ax2.set_ylabel('Magnitude (dB)')
ax2.set_xlim( 0, Fs//2)
magnitude_limits = [-80, 60]  # in dB
ax2.set_ylim(magnitude_limits)
ax2.grid(True)

# Adjust vertical spacing between subplots
plt.subplots_adjust(hspace=0.4)

def read_data_from_serial(n):
    samples = []
    err_count = 0
    while len(samples) < n:
        try:
            line = ser.readline()
        except ValueError:
            print( "No data received from serial..")
            return None
        try:
            # Read a line from serial and convert to float
            line = line.decode().strip()
            if line[0:3] == "+++":
                samples.clear()
                continue
            value = float(line)
            if value > 3300:
                value = 3300
            elif value < 0:
                value = 0
            samples.append(value)
        except ValueError as ex:
            err_count += 1
            #print(ex, err_count)
            if err_count >= 3:
                return None
    return samples

def read_test_data(n):
    f0 = 500  # The fundamental frequency 
    A = 1000  # The amplitude
    noise = 0.01*np.random.uniform(-1,1,size=n)
    data = A*( 1.67 + np.sin( 2*np.pi*f0*np.arange(n)/Fs) 
                 + noise ).astype(np.float32)
    return data

saved_data = np.zeros(N)

# Function to update the plot
def update_plot(frame):
    global data1, data2
    # Use generated data.
    #data = read_test_data(N)
    # Read the data from the serial port
    data = read_data_from_serial(N)
    if data is None:
        data = saved_data
    # Apply a window function (Hanning window)
    window_coeffs = np.hanning(N)
    windowed_data = data * window_coeffs
    # Compute FFT
    spectrum = np.fft.fft(windowed_data)/N
    # Update time-domain plot
    data1.set_ydata(data)
    # Update frequency spectrum in logarithmic scale (dB)
    spectrum_mag = 20*np.log10( np.abs(spectrum[:N//2])+1e-6 )
    data2.set_ydata( spectrum_mag[:N//2] )
    return data1, data2

# Function to handle Ctrl+C (KeyboardInterrupt)
def signal_handler(signal, frame):
    print("Ctrl+C detected. Terminating the program.")
    plt.close('all')  # Close all matplotlib windows
    ser.close()       # Close serial port
    exit(0)

# Register the signal handler
signal.signal(signal.SIGINT, signal_handler)

# Create animation (set the update interval in msec)
ani = FuncAnimation( fig, update_plot, blit=True, 
                     interval=25, save_count=1 )

# Show the plot in non-blocking mode
plt.show()

หากทดสอบการทำงานของระบบ โดยใช้สัญญาณเสียงความถี่คงที่ เช่น Sine Wave 1kHz จะได้ผลในลักษณะต่อไปนี้

รูป: ตัวอย่างสเปกตรัมความถี่ของสัญญาณเสียง เมื่อทดสอบกับสัญญาณที่มีความถี่ 1kHz

รูป: ตัวอย่างสเปกตรัมความถี่ของสัญญาณเสียง เมื่อทดสอบกับสัญญาณที่มีความถี่ 440Hz

 


กล่าวสรุป#

บทความนี้นำเสนอ ตัวอย่างการเขียนโค้ด Arduino-ESP32 เพื่ออ่านค่าจากโมดูลเซนเซอร์เสียงแบบแอนะล็อก และส่งข้อมูลเข้าคอมพิวเตอร์ผู้ใช้ผ่านทางพอร์ต Serial และเขียนโค้ด Python เพื่อรับข้อมูลจากบอร์ด ESP32 นำมาแสดงรูปกราฟ

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

 


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

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