ข้ามไปยังเนื้อหา

ไมโครโฟน PDM สเตอริโอและ level meter

  1. เก็บสัญญาณจากไมโครโฟน PDM สเตอริโอ และคำนวณระดับเสียงซ้าย/ขวา
  2. แสดงระดับเสียงเป็น level meter และอธิบายว่าคำนวณแบบ peak หรือ RMS
  3. ทดสอบด้วยเสียงจากซ้ายและขวา แล้วยืนยันว่าช่องสัญญาณไม่สลับกัน

PDM (Pulse Density Modulation) คือสตรีม 1 บิตความเร็วสูง (1–3 MHz) ที่ ความหนาแน่นของ pulse แทนขนาดสัญญาณ ต่างจาก PCM ที่แต่ละตัวอย่างเป็นตัวเลขหลายบิต (16/24-bit) ที่อัตราต่ำกว่ามาก (8–48 kHz) ไมค์ MEMS ขนาดเล็กส่ง PDM ออกมาได้ตรง ๆ เพราะวงจรภายในเรียบง่ายกว่า ชิป PSoC Edge มี PDM/PCM converter แปลง PDM เป็น PCM ให้ในฮาร์ดแวร์ (lowpass + decimate) ก่อนส่งเข้า FIFO ให้ CPU อ่าน — episode นี้ตั้งค่าไว้ที่ 16 kHz, เฟรมละ 160 ตัวอย่างต่อช่อง (PDM_MIC_FRAME_SAMPLES_PER_CHANNEL = 160) ซึ่งพอดี 10 ms ต่อเฟรมที่ 16 kHz

การจับสัญญาณจริงคือ interrupt อ่าน FIFO + double-buffer ซอฟต์แวร์ ไม่ใช่ DMA ตามที่ README ต้นทางอธิบาย

หัวข้อที่มีชื่อว่า “การจับสัญญาณจริงคือ interrupt อ่าน FIFO + double-buffer ซอฟต์แวร์ ไม่ใช่ DMA ตามที่ README ต้นทางอธิบาย”

README ต้นทางบอกว่า “DMA-driven — CPU ไม่ต้องยุ่ง, hardware เขียนลง circular buffer” และ “ผูก DMA channel → IRQ handler” แต่โค้ดจริงที่ commit 9a8e3ed ไม่ใช้ DMA เลย — ใช้ interrupt ของแต่ละช่อง (ซ้าย = channel 2, ขวา = channel 3) ที่ยิงเมื่อ FIFO ถึง trigger level (PDM_RX_FIFO_TRIG_LEVEL = ครึ่งหนึ่งของขนาด FIFO) แล้ว process_channel_irq() อ่านค่าออกจาก FIFO ทีละตัวอย่างด้วย Cy_PDM_PCM_Channel_ReadFifo() ใส่ลง buffer ที่กำลัง เขียนอยู่ (buffer0/buffer1 สลับกันแบบ ping-pong ที่ซอฟต์แวร์จัดการเอง ไม่ใช่ DMA descriptor) เมื่อครบ 160 ตัวอย่างทั้งสองช่อง (PDM_READY_BOTH) จะส่ง semaphore ปลุก task ที่รออยู่ให้ไปคำนวณระดับเสียงต่อ — CPU จึงต้อง “ยุ่ง” กับทุกตัวอย่างจริง ๆ ผ่าน interrupt ไม่ใช่แค่รอ DMA เสร็จ

ค่าเฉลี่ยที่คำนวณคือ mean of |x| ไม่ใช่ RMS แม้ทำหน้าที่คล้ายกัน

หัวข้อที่มีชื่อว่า “ค่าเฉลี่ยที่คำนวณคือ mean of |x| ไม่ใช่ RMS แม้ทำหน้าที่คล้ายกัน”

compute_level() คำนวณ peak_abs (ค่าสูงสุดของ |sample|) และ avg_abs = ผลรวม |sample| หารด้วยจำนวนตัวอย่าง — เป็นค่าเฉลี่ยของค่าสัมบูรณ์ ไม่มีการยกกำลังสองหรือถอดรากที่สองเลย จึงไม่ใช่ RMS (root-mean-square) แม้จะขึ้น ลงตามความดังคล้ายกันและคำนวณเบากว่า (ไม่มีการคูณ) แต่ให้ค่าต่ำกว่า RMS ของสัญญาณเดียวกันเสมอ (คลื่นไซน์บริสุทธิ์ mean|x| ≈ 0.9 เท่าของ RMS)

เปอร์เซ็นต์ที่ขึ้นจอไม่ได้มาจาก peak/32767 ตรง ๆ แต่ map ผ่าน floor/ceiling ที่ปรับให้เหมาะกับเสียงพูดในห้องเรียน

หัวข้อที่มีชื่อว่า “เปอร์เซ็นต์ที่ขึ้นจอไม่ได้มาจาก peak/32767 ตรง ๆ แต่ map ผ่าน floor/ceiling ที่ปรับให้เหมาะกับเสียงพูดในห้องเรียน”

to_ui_pct() ไม่ได้หาร avg_abs ด้วย INT16_MAX (32767) ตรง ๆ ตามที่ README ต้นทางอธิบายไว้ (และสูตรนั้นในอันที่ จริงก็ใช้ peak ไม่ใช่ avg) — โค้ดจริง map avg_abs แบบ clamp เชิงเส้นระหว่าง PDM_UI_FLOOR_ABS = 80 (ให้ 0%) กับ PDM_UI_CEIL_ABS = 8000 (ให้ 100%) คอมเมนต์ในซอร์สระบุเหตุผลตรง ๆ ว่า “Tuned for classroom speech level so UI% doesn’t saturate too early” — เสียงพูดปกติในห้องเรียนมีขนาดเล็กกว่าค่า full-scale ของ int16 มาก ถ้าหารด้วย 32767 ตรง ๆ แถบจะขึ้นแค่ไม่กี่ % เสมอ ต้อง “บีบ” ช่วงที่ใช้งานจริงให้เต็มแถบ

balance meter ถูกทำไว้แล้วจริง ไม่ใช่แค่หัวข้อ “ลองแก้” ตามที่ README ต้นทางแนะนำ

หัวข้อที่มีชื่อว่า “balance meter ถูกทำไว้แล้วจริง ไม่ใช่แค่หัวข้อ “ลองแก้” ตามที่ README ต้นทางแนะนำ”

README ต้นทางแนะนำในหัวข้อ Experiment Ideas ว่า “Balance meter — แสดง (L-R)/(L+R) เป็นเข็ม bar กลางจอ” ราวกับเป็น สิ่งที่ยังไม่มี แต่โค้ดจริงคำนวณ balance_lr = (L_avg - R_avg) * 100 / (L_avg + R_avg) ไว้แล้วใน pdm_probe_logger.c และ view แสดงตัวอักษร “L” เมื่อ balance > 3 หรือ “R” เมื่อ < -3 ฟีเจอร์นี้จึงพร้อมใช้ตั้งแต่ build แรก ไม่ต้องเขียนเพิ่ม

UI ใช้ lv_timer poll แบบ “sample ล่าสุดชนะ” ที่ 50 ms ไม่ใช่ lv_async_call ที่ 50 Hz

หัวข้อที่มีชื่อว่า “UI ใช้ lv_timer poll แบบ “sample ล่าสุดชนะ” ที่ 50 ms ไม่ใช่ lv_async_call ที่ 50 Hz”

เหมือน pattern ที่พบซ้ำในบทเรียน 2.5 (WiFi scan): mic_presenter.c ไม่ใช้ lv_async_call() ตามที่ README ต้นทาง อธิบาย (“lv_async_call ในจังหวะ 50 Hz”) แต่สร้าง lv_timer ที่คาบ 50 ms (เท่ากับ 20 ครั้ง/วินาที ไม่ใช่ 50 ครั้ง/วินาทีตามที่ระบุ) มาอ่าน sample ล่าสุดที่ถูกเขียนไว้ภายใต้ taskENTER_CRITICAL()/EXIT ผู้ผลิต (logger task) ส่งค่าใหม่ทุก 10 ms แต่ policy คือ “sample ล่าสุดชนะ” (เขียนทับของเก่า) — เสียงกระแทกสั้น ๆ ที่เกิดและหายไป ภายในกรอบ 10 ms หนึ่งเฟรมอาจไม่มีวันถูก UI เห็นเลย ถ้า timer 50 ms มาอ่านหลังจากเฟรมนั้นถูกเขียนทับไปแล้ว

โค้ดของ episode นี้อยู่ใน Developer Hub (อ้างอิงที่ commit 9a8e3ed) — อ่าน Why ของ README ต้นทาง เพื่อเข้าใจ PDM/PCM แต่ โค้ดตัวอย่างด้านล่างคัดลอกจากไฟล์จริง (Apache-2.0, tesaiot/developer-hub, commit เดียวกัน) เพราะกลไกจับสัญญาณและสูตรคำนวณต่างจากที่ README ต้นทางอธิบาย

app_audio/pdm/pdm_probe_logger.c — mean-abs ไม่ใช่ RMS และเปอร์เซ็นต์ที่ clamp floor/ceiling:

/* Tuned for classroom speech level so UI% doesn't saturate too early. */
#define PDM_UI_FLOOR_ABS (80U)
#define PDM_UI_CEIL_ABS (8000U)
static uint32_t to_ui_pct(uint32_t avg_abs)
{
if (avg_abs <= PDM_UI_FLOOR_ABS) { return 0U; }
if (avg_abs >= PDM_UI_CEIL_ABS) { return 100U; }
return ((avg_abs - PDM_UI_FLOOR_ABS) * 100U) / (PDM_UI_CEIL_ABS - PDM_UI_FLOOR_ABS);
}

app_audio/pdm/pdm_mic.c — จับสัญญาณด้วย interrupt อ่าน FIFO ไม่ใช่ DMA:

static void process_channel_irq(pdm_channel_state_t *state, uint8_t channel_index, uint8_t ready_bit)
{
/* ... */
for (uint32_t index = 0U; index < PDM_RX_FIFO_TRIG_LEVEL; index++)
{
int16_t sample = (int16_t)Cy_PDM_PCM_Channel_ReadFifo(CYBSP_PDM_HW, channel_index);
/* เขียนลง state->active[out_idx++] จนครบเฟรม แล้วสลับ active/full */
}
Cy_PDM_PCM_Channel_ClearInterrupt(CYBSP_PDM_HW, channel_index, CY_PDM_PCM_INTR_RX_TRIGGER);
}

app_ui/mic/mic_presenter.c — poll ทุก 50 ms แบบ latest-sample-wins:

static void mic_presenter_ui_timer_cb(lv_timer_t *timer)
{
mic_presenter_sample_t local = {0};
bool has_sample = false;
taskENTER_CRITICAL();
has_sample = s_has_sample;
if (has_sample) { local = s_latest_sample; }
taskEXIT_CRITICAL();
if (has_sample && s_view_ready) { mic_view_apply(&local); }
}
  • main_example.c เรียก mic_presenter_start() ก่อน pdm_probe_logger_start() ตรงตามที่ README ต้นทางอธิบาย (สร้าง consumer ก่อน producer)
  • ดูโฟลเดอร์เต็มที่ int_ep06_digital_mic_probe/
  • คิดว่าใช้ DMA ตามที่ README ต้นทางอธิบาย — โค้ดจริงใช้ interrupt อ่าน FIFO ทีละตัวอย่างพร้อม double-buffer ที่ซอฟต์แวร์จัดการเอง ให้ยึดโค้ดจริงเมื่ออธิบายกลไกจับสัญญาณ
  • เข้าใจว่า avg ที่คำนวณคือ RMS — เป็นแค่ค่าเฉลี่ยของค่าสัมบูรณ์ (mean|x|) ไม่มีการยกกำลังสอง ให้ค่าต่ำกว่า RMS เสมอสำหรับสัญญาณเดียวกัน
  • คิดว่า UI % มาจาก avg*100/32767 ตรง ๆ — จริงถูก clamp ผ่านช่วง 80–8000 ที่ปรับให้เหมาะกับเสียงพูดเบา ๆ ใน ห้องเรียน ค่าที่เบากว่า 80 ขึ้น 0% เสมอ และค่าที่แรงกว่า 8000 ขึ้น 100% เสมอ
  • คิดว่า UI จะจับเสียงกระแทกสั้น ๆ ได้ทุกครั้ง — policy “sample ล่าสุดชนะ” ที่ UI timer 50 ms อาจพลาดเฟรม 10 ms ที่มีเสียงกระแทกไปเลยถ้าถูกเฟรมถัดไปเขียนทับก่อน UI มาอ่าน
Terminal window
# ในโฟลเดอร์ master template (ดูบทเรียน 1.1)
# 1) ลบไฟล์ของ episode เก่าใน proj_cm55/apps/
# 2) คัดลอกไฟล์ทั้งหมดของ episode นี้ลงใน proj_cm55/apps/
make build
make program # flash ผ่าน KitProg3

หรือเปิด ตัวอย่างนี้บน Developer Hub แล้ว flash เฟิร์มแวร์สำเร็จรูป

หน้าจอของ EP06 — Digital Mic Probe บน TESAIoT Dev Kit

ก่อนอ่านโค้ด ให้ทายว่าหน้าจอนี้มี object อะไรบ้าง และอะไรเปลี่ยนเมื่อผู้ใช้แตะหรือเมื่อค่าเซนเซอร์เปลี่ยน

  1. ทาย ก่อนแก้: เลือกค่าหนึ่งค่าที่ README ของตัวอย่างอธิบายไว้ในส่วน How แล้วเขียนว่าจะเห็นอะไรเปลี่ยนบนจอหรือใน log
  2. แก้และรัน build + flash แล้วเทียบกับที่ทายไว้ ถ้าไม่ตรง ให้หาว่าเข้าใจส่วนไหนผิด
  3. ทำเพิ่ม ต่อยอดหนึ่งอย่างที่ตัวอย่างยังไม่มี แล้วเก็บภาพหรือวิดีโอไว้ใน portfolio
  • PDM ต่างจาก PCM อย่างไร
  • RMS กับ peak ให้ภาพระดับเสียงต่างกันอย่างไร
  • ทดสอบอย่างไรว่าช่องซ้ายและขวาไม่สลับกัน

คำตอบอยู่ใน README ของตัวอย่างและในโค้ด ถ้าตอบข้อใดไม่ได้ ให้กลับไปอ่านส่วน Why / What / How อีกครั้ง

คำถามทบทวน

ลองตอบเองก่อน แล้วค่อยเปิดดูเฉลย

  1. pdm_probe_logger_task รู้ได้อย่างไรว่ามีเสียงเฟรมใหม่ให้คำนวณ (เป้าหมายข้อ 1)

    1. poll FIFO ของ PDM ทุก 1 ms
    2. ISR ของแต่ละช่องอ่าน FIFO ครั้งละ 32 ตัวอย่างเติม buffer ที่กำลังเขียน เมื่อครบ 160 ตัวอย่าง (10 ms ที่ 16 kHz) ทั้งซ้ายและขวา จะสลับ ping-pong buffer แล้ว set semaphore ที่ task รออยู่
    3. LVGL timer เรียก task ทุก 50 ms
    4. DMA เขียนค่าตรงเข้า label บนจอ
    ดูเฉลย

    คำตอบ: B. ISR ของแต่ละช่องอ่าน FIFO ครั้งละ 32 ตัวอย่างเติม buffer ที่กำลังเขียน เมื่อครบ 160 ตัวอย่าง (10 ms ที่ 16 kHz) ทั้งซ้ายและขวา จะสลับ ping-pong buffer แล้ว set semaphore ที่ task รออยู่

    process_channel_irq() อ่าน FIFO เมื่อถึง trigger level (32) นับ block_count จนครบ PDM_INTERRUPTS_PER_FRAME แล้วสลับ active/full buffer เมื่อทั้งสองช่องพร้อม (PDM_READY_BOTH) จึงเรียก cy_rtos_semaphore_set() ฝั่ง pdm_mic_get_frame() บล็อกรอ semaphore นั้น CPU จึงคำนวณเป็นก้อน 10 ms แทนการอ่านทีละตัวอย่าง (README ของ episode พูดถึง DMA แต่โค้ดที่ commit นี้ใช้ interrupt อ่าน FIFO)

  2. แถบ “UI Level” ของแต่ละช่องคำนวณจากอะไร (เป้าหมายข้อ 2)

    1. ค่า RMS ของเฟรม
    2. ค่า peak ของเฟรม
    3. ค่าเฉลี่ยของค่าสัมบูรณ์ (mean |x|) ของเฟรม 10 ms แล้วแมปช่วง 80–8000 เป็น 0–100%
    4. จำนวนตัวอย่างที่ไม่เป็นศูนย์
    ดูเฉลย

    คำตอบ: C. ค่าเฉลี่ยของค่าสัมบูรณ์ (mean |x|) ของเฟรม 10 ms แล้วแมปช่วง 80–8000 เป็น 0–100%

    compute_level() คำนวณทั้ง peak_abs และ avg_abs = ผลรวม |x| หารจำนวนตัวอย่าง ไม่มีการยกกำลังสองจึงไม่ใช่ RMS แล้ว to_ui_pct() แมป avg_abs จาก PDM_UI_FLOOR_ABS (80) ถึง PDM_UI_CEIL_ABS (8000) mean |x| ขึ้นลงตามความดังเหมือน RMS และคำนวณเบากว่า แต่ได้ค่าต่ำกว่า RMS สำหรับสัญญาณเดียวกัน (คลื่น sine ได้ราว 0.9 เท่าของ RMS)

  3. ตบมือหนึ่งครั้งใกล้ไมโครโฟน แถบ Peak (Hold) จะมีพฤติกรรมอย่างไร (เป้าหมายข้อ 2)

    1. กระโดดขึ้นทันที ค้างไว้ราว 1.5 วินาที แล้วค่อยลดลง 2% ทุก 100 ms ส่วนแถบค่าเฉลี่ยตกลงเร็วกว่ามาก
    2. ขึ้นช้า ๆ ตามค่าเฉลี่ย
    3. ขึ้นแล้วค้างจนกว่าจะรีเซ็ตบอร์ด
    4. ไม่ขยับ เพราะ peak ถูกกรองออก
    ดูเฉลย

    คำตอบ: A. กระโดดขึ้นทันที ค้างไว้ราว 1.5 วินาที แล้วค่อยลดลง 2% ทุก 100 ms ส่วนแถบค่าเฉลี่ยตกลงเร็วกว่ามาก

    update_peak_hold() รับค่าใหม่ถ้าสูงกว่า hold ค้างไว้ MIC_PEAK_HOLD_MS = 1500 ms แล้วลด MIC_PEAK_DECAY_STEP_PCT = 2% ทุก MIC_PEAK_DECAY_INTERVAL_MS = 100 ms peak บอกว่าสัญญาณเข้าใกล้ขีดจำกัด (clipping) หรือยัง ส่วนค่าเฉลี่ยบอกความดังโดยรวม จึงแสดงคู่กัน

  4. logger ส่งค่าทุก 10 ms แต่ UI timer อ่านทุก 50 ms แบบ latest-sample-wins เสียงกระแทกที่สั้นกว่า 10 ms อาจไม่ขึ้นบนแถบ peak เพราะอะไร (เป้าหมายข้อ 2)

    1. เพราะไมโครโฟน PDM ตอบสนองช้ากว่า 10 ms
    2. เพราะ LVGL วาดไม่ทัน
    3. เพราะ ISR ทิ้งเฟรมที่ดังเกินไป
    4. เพราะเฟรมที่มีเสียงกระแทกอาจถูกเฟรมถัดไปเขียนทับก่อน UI มาอ่าน UI เห็นราว 1 ใน 5 เฟรม ถ้าต้องจับ peak ทุกครั้งต้องสะสมค่าสูงสุดไว้ฝั่ง producer ระหว่างรอบที่ UI อ่าน
    ดูเฉลย

    คำตอบ: D. เพราะเฟรมที่มีเสียงกระแทกอาจถูกเฟรมถัดไปเขียนทับก่อน UI มาอ่าน UI เห็นราว 1 ใน 5 เฟรม ถ้าต้องจับ peak ทุกครั้งต้องสะสมค่าสูงสุดไว้ฝั่ง producer ระหว่างรอบที่ UI อ่าน

    mic_presenter_publish_sample() เขียน s_latest_sample ทับทุกครั้ง ส่วน mic_presenter_ui_timer_cb() อ่านทุก MIC_UI_TIMER_PERIOD_MS = 50 ms นโยบายนี้ทำให้ UI ไม่บล็อกและไม่มีคิวค้าง แต่แลกกับการพลาดเหตุการณ์ที่สั้นมาก ต้องเลือกให้เหมาะกับงาน

  5. ดีดนิ้วใกล้ไมโครโฟนฝั่งซ้ายของบอร์ด ค่า Balance ควรเป็นอย่างไร และถ้าได้ผลตรงข้ามแปลว่าอะไร (เป้าหมายข้อ 3)

    1. Balance เป็นลบและขึ้น “R” เสมอ เพราะสูตรคือ R − L
    2. Balance เป็น 0 เพราะไมค์สองตัวอยู่ใกล้กัน
    3. Balance เป็นบวกและขึ้น “L” ถ้ากลับเป็น “R” แปลว่าการจับคู่ช่องกับไมโครโฟนสลับกัน ต้องตรวจว่าช่อง PDM ที่ตั้งเป็นซ้าย/ขวา (channel 2/3) ตรงกับไมค์ตัวจริงบนบอร์ด
    4. Balance ขึ้นกับความดังรวม ไม่ใช่ทิศ
    ดูเฉลย

    คำตอบ: C. Balance เป็นบวกและขึ้น “L” ถ้ากลับเป็น “R” แปลว่าการจับคู่ช่องกับไมโครโฟนสลับกัน ต้องตรวจว่าช่อง PDM ที่ตั้งเป็นซ้าย/ขวา (channel 2/3) ตรงกับไมค์ตัวจริงบนบอร์ด

    balance_lr = (L_avg − R_avg) × 100 / (L_avg + R_avg) และ view ขึ้น L เมื่อ > 3, R เมื่อ < −3 การทดสอบที่ดีควรทำทั้งสองฝั่งสลับกันหลายครั้งในห้องเงียบ เพราะไมค์ทั้งสองอยู่ใกล้กัน ความต่างอาจไม่มากถ้าแหล่งเสียงอยู่ไกล

อ้างอิงบทเรียนนี้

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

"ไมโครโฟน PDM สเตอริโอและ level meter" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0

ข้อความอ้างอิงภาษาอังกฤษ: "Stereo PDM microphone and a level meter" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0

ลิงก์บทเรียน: https://tesaiot.github.io/tesa-qualification-program/courses/tesaiot-firmware-stack/m03-interactive-sensors/l06-digital-mic-probe/

บทเรียนนี้ดัดแปลงจากต้นฉบับด้านล่าง เมื่ออ้างอิงให้คงเครดิตต้นฉบับไว้ด้วย
https://github.com/tesaiot/developer-hub/blob/9a8e3ed1d813bfd67fabf6b7ac15c6ff9750b465/int_ep06_digital_mic_probe · Code stays in the Developer Hub and is linked at pinned commits, never copied: the episodes, practice codes and main-branch examples are Apache-2.0; the master template and the OPTIGA client carry Infineon/Cypress EULAs.

วิธีอ้างอิง TESA ฉบับเต็ม

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

เนื้อหาเผยแพร่ภายใต้ CC BY-NC 4.0 นำไปใช้ต่อในงานที่ไม่ใช่เพื่อการค้าได้ โปรดอ้างอิงสมาคมสมองกลฝังตัวไทย (TESA) ทุกครั้ง · วิธีอ้างอิง TESA