// MAX9814 test - read OUT on A0 and compute baseline, peak and RMS envelope
// คำอธิบายสั้น ๆ: โปรแกรมนี้อ่านค่าอนาล็อกจากพิน A0 เป็นชุดตัวอย่างในช่วงเวลาสั้นๆ (sampleWindowMs)
// แล้วคำนวณค่าเฉลี่ย (DC baseline), ค่าพีคทูพีค, และค่า RMS (AC envelope) เพื่อแสดงผลทาง Serial (CSV-like)
const int pinA = A0; // พิน ADC บน Arduino ที่เชื่อมต่อกับ OUT ของ MAX9814
const int sampleWindowMs = 50; // ระยะเวลาการเก็บตัวอย่าง (ms) — ยิ่งมากก็ยิ่งได้ค่า RMS/peak แม่นขึ้น แต่ตอบสนองช้าลง
const unsigned long serialBaud = 115200;// ความเร็วพอร์ตอนุกรม (baud rate) สำหรับ Serial Monitor / Serial Plotter
void setup() {
Serial.begin(serialBaud); // เริ่ม Serial communication ที่ความเร็วที่กำหนด เพื่อส่งข้อมูลไปยังคอมพิวเตอร์
analogReference(DEFAULT); // ตั้งค่า reference ของ ADC ให้เท่ากับ Vcc (ปกติคือ 5V หรือ 3.3V ขึ้นกับบอร์ด)
delay(50); // หน่วงสั้นๆ ให้วงจรและ Serial port มีเวลาสตาร์ท/นิ่งก่อนอ่านค่า
Serial.println("MAX9814 test start"); // พิมพ์ข้อความเริ่มต้น เพื่อให้รู้ว่าโปรแกรมเริ่มทำงานแล้ว
}
void loop() {
unsigned long start = millis(); // เก็บเวลาปัจจุบัน (ms) เพื่อใช้เป็นจุดเริ่มต้นของหน้าต่างการเก็บตัวอย่าง
unsigned long endTime = start + sampleWindowMs; // คำนวณเวลาสิ้นสุด = เวลาเริ่ม + ขนาดหน้าต่าง (อาจระวัง overflow — ดูหมายเหตุด้านล่าง)
unsigned long sampleCount = 0; // ตัวนับจำนวนตัวอย่างที่อ่านได้ (ใช้ในการคำนวณค่าเฉลี่ยและ RMS)
long sum = 0; // ผลรวมของค่าที่อ่าน (sum of ADC counts) — ใช้หาค่าเฉลี่ย (DC baseline)
long sumSq = 0; // ผลรวมของค่ากำลังสอง (sum of squares) — ใช้คำนวณ RMS
int minVal = 1023; // เก็บค่า minimum ที่อ่านได้ภายในหน้าต่าง (กำหนดเริ่มต้นที่ค่าสูงสุดของ ADC)
int maxVal = 0; // เก็บค่า maximum ที่อ่านได้ภายในหน้าต่าง (เริ่มที่ค่างวดต่ำสุด)
while (millis() < endTime) { // เก็บตัวอย่างต่อเนื่องจนกว่าเวลาจะถึง endTime
int v = analogRead(pinA); // อ่านค่า ADC (0..1023 สำหรับ 10-bit ADC ของ Arduino UNO/Nano)
sum += v; // บวกค่าลงในผลรวมเพื่อคำนวณค่าเฉลี่ยทีหลัง
sumSq += (long)v * (long)v; // บวกค่ากำลังสองลงใน sumSq — cast เป็น long ก่อนคูณเพื่อหลีกเลี่ยง overflow ของ int
if (v < minVal) minVal = v; // อัปเดตค่าน้อยสุด (min)
if (v > maxVal) maxVal = v; // อัปเดตค่ามากสุด (max)
sampleCount++; // เพิ่มตัวนับตัวอย่าง
}
// average baseline (DC)
float avg = (float)sum / (float)sampleCount; // แปลงผลรวมเป็นค่าเฉลี่ย (เป็นหน่วย ADC counts เช่น 0..1023)
// RMS (includes DC), so subtract DC to get AC RMS:
float meanSq = (float)sumSq / (float)sampleCount; // คำนวณค่า mean of squares
float rms = sqrt(meanSq - avg * avg); // RMS ของส่วน AC = sqrt(E[x^2] - (E[x])^2)
// หมายเหตุ: sqrt ของค่าลบจะเป็น NaN — ป้องกันโดยเช็คก่อนในเวอร์ชัน production
int peakToPeak = maxVal - minVal; // คำนวณพีคทูพีค (difference ระหว่างค่าสูงสุดกับค่าต่ำสุด ในช่วงตัวอย่าง)
// Convert ADC counts to voltage (for UNO 5V): adjust if using 3.3V board
const float Vref = 5.0; // แรงดันอ้างอิงสำหรับการแปลงเป็นโวลต์ — ถ้าใช้บอร์ด 3.3V ให้เปลี่ยนเป็น 3.3
float volts_per_count = Vref / 1023.0; // แปลง 1 count = เท่าไหร่เป็นโวลต์ (10-bit ADC: 0..1023)
float dcVoltage = avg * volts_per_count; // DC baseline ในหน่วยโวลต์
float rmsVoltage = rms * volts_per_count; // RMS (AC) ในหน่วยโวลต์
float vpp = peakToPeak * volts_per_count; // peak-to-peak ในหน่วยโวลต์
// Print CSV-like for Serial Plotter: time, dc, vpp, rms
Serial.print(millis()); // พิมพ์เวลาปัจจุบัน (ms) — คอลัมน์ 1 (ช่วยให้ Serial Plotter แกนเวลา)
Serial.print(','); // คั่นคอลัมน์ด้วย comma (CSV-like)
Serial.print(dcVoltage, 3); // พิมพ์ DC voltage (3 จุดทศนิยม) — คอลัมน์ 2
Serial.print(','); // คั่น
Serial.print(vpp, 3); // พิมพ์ Vpp (3 จุดทศนิยม) — คอลัมน์ 3
Serial.print(','); // คั่น
Serial.print(rmsVoltage, 4); // พิมพ์ RMS voltage (4 จุดทศนิยม) — คอลัมน์ 4
Serial.print(','); // คั่น
Serial.print(peakToPeak); // พิมพ์ peak-to-peak (ADC counts) — คอลัมน์ 5 (จำนวนเต็ม)
Serial.print(','); // คั่น
Serial.print((int)avg); // พิมพ์ค่าเฉลี่ยในหน่วย ADC (จำนวนเต็ม) — คอลัมน์ 6
Serial.println(); // ขึ้นบรรทัดใหม่ — เสร็จแถวข้อมูลเดียว
// small pause to control update rate
delay(10); // หน่วงสั้นๆ เพื่อควบคุมอัตราการอัปเดต (รวมเวลาที่ใช้ใน while ทำให้ loop ไม่เร็วเกินไป)
/*
// Print CSV-like for Serial Plotter: time, dc, vpp, rms
Serial.print("Time = ");
Serial.print(millis());
Serial.print(" dcVoltage = ");
Serial.print(dcVoltage, 3);
Serial.print(" vpp = ");
Serial.print(vpp, 3);
Serial.print(" rmsVoltage = ");
Serial.print(rmsVoltage, 4);
Serial.print(" peakToPeak = ");
Serial.print(peakToPeak);
Serial.print(" avg = ");
Serial.println(avg);
// small pause to control update rate
delay(10); // หน่วงสั้นๆ เพื่อควบคุมอัตราการอัปเดต (รวมเวลาที่ใช้ใน while ทำให้ loop ไม่เร็วเกินไป)
*/
}
/*
คำแนะนำ:
-ถ้าใช้บอร์ด 3.3V ให้เปลี่ยน Vref = 3.3.
-เปิด Serial Plotter ใน Arduino IDE แล้วเลือก 115200 baud — คุณจะเห็นเส้นกราฟของ DC, Vpp, RMS ตามคอลัมน์ที่ส่งออก
--------------------------------------------------------------------------------------------------------
ถ้าคุณมี oscilloscope — ขั้นตอนดูสัญญาณ
1.Probe ที่ OUT และ GND.
2.คาดว่าจะเห็น waveform ที่มี DC bias ~1.0–1.3 V (ขึ้นอยู่กับบอร์ด)
และ AC สวิง (± ~1 V หรือ ~2 Vpp บนสภาพเสียงดังในระดับใกล้ไมค์) — ขึ้นกับ gain และ supply.
Adafruit
3.เปลี่ยนระดับเสียง (พูดใกล้/ไกล, ตบมือ) แล้วสังเกต AGC ทำงาน —
สัญญาณ p-p จะไม่เพิ่มตามสัดส่วนเมื่อ AGC เข้ามาควบคุม (จะเห็นการลดลงของ gain เมื่อเสียงดังมาก).
--------------------------------------------------------------------------------------------------------
ทดสอบจริง ๆ ให้ลองแบบนี้ (เช็คลิสต์)
1.ต่อสายตามด้านบน → เปิด Serial Plotter.
2.พูดใกล้ไมค์ → ดู RMS และ Vpp ขึ้นทันทีหรือไม่ (ค่าสูงขึ้น)
3.พูดเบา ๆ → ค่า RMS ต่ำลง — AGC น่าจะเพิ่มเกนให้ถ้าระดับต่ำมาก ๆ
4.ส่งสัญญาณดัง (ตะโกนหรือพูดใกล้) → สังเกตว่า Vpp ถูกจำกัด (ถ้า AGC ทำงาน)
5.เปลี่ยน GAIN pin (open / GND / VCC) แล้วดูการตอบสนอง —
ค่า RMS / Vpp ควรเปลี่ยนตามระดับ gain ที่เลือก.
/////////////////////////////////////////////////////////////////////////////////////
คำอธิบายเชิงลึก (อธิบายแนวคิดสำคัญที่โค้ดใช้)
1.การเก็บตัวอย่างเป็นหน้าต่างเวลา (sampleWindowMs)
-โค้ดอ่านค่า ADC หลาย ๆ ครั้งต่อเนื่องภายใน sampleWindowMs มิลลิวินาที
แล้วคำนวณสถิติเช่น mean / RMS / peak-to-peak จากชุดข้อมูลนั้น
-ข้อดี: ได้ค่าเชิงสถิติที่มีความหมายภายในช่วงเวลาสั้น ๆ (เช่น 50 ms) —
หมาะสำหรับแสดงระดับเสียงเป็นกราฟ
-trade-off: ถ้าตั้ง sampleWindowMs ใหญ่ ค่าจะนิ่งขึ้นแต่ตอบสนองช้าลง
ถ้าตั้งเล็ก โปรแกรมตอบสนองเร็วแต่ค่าที่ได้อาจมีเสียง (noise) มาก
2.การคำนวณ RMS แบบลบ DC (AC RMS)
-meanSq = E[x^2] และ avg = E[x]
-AC RMS (เฉพาะส่วนสลับของสัญญาณ) = sqrt( E[x^2] - (E[x])^2 )
-เหตุผล: สัญญาณที่อ่านจากไมค์มี DC bias (ค่า baseline อยู่ครึ่งสเกล) —
เราต้องการวัดขนาดของส่วน AC (ความดังของเสียง) ไม่ใช่ DC bias
3.peak-to-peak
-เก็บ min และ max ระหว่างหน้าต่างตัวอย่าง เพื่อวัด amplitude
สูงสุดที่ปรากฏ (ง่ายต่อการสังเกต clipping)
4.การแปลง ADC → โวลต์
-ADC 10-bit ส่งค่าระหว่าง 0..1023 ดังนั้น 1 count = Vref / 1023 โวลต์
-ต้องระวัง: Vref ต้องเท่ากับ reference ที่ ADC ใช้ (DEFAULT=Vcc) —
ถ้าใช้ 3.3V ให้เปลี่ยนเป็น 3.3 เพื่อให้ผลเป็นโวลต์ถูกต้อง
5.การพิมพ์เป็น CSV สำหรับ Serial Plotter
-รูปแบบ time,dc,vpp,rms,peakToPeak,avgADC
ทำให้ Arduino IDE >> Serial Plotter สามารถวาดหลายเส้นพร้อมกัน — ดูได้สะดวก
---------------------------------------------------------------------------------------------------------------
หมายเหตุและข้อเสนอแนะ (ปรับปรุงความปลอดภัย/ความแม่นยำ)
1.ปัญหา millis() overflow (wrapping) — แนวทางที่ปลอดภัยกว่า
-ปัจจุบันโค้ดใช้ endTime = start + sampleWindowMs;
while (millis() < endTime) ... ซึ่งมักปลอดภัยในหน้าต่างสั้น ๆ
แต่ถ้า millis() เกิด wrap-around ระหว่างนั้น (เกิดหลัง ~49.7 วัน)
เงื่อนไขอาจทำงานไม่ถูกต้อง
-รูปแบบที่ปลอดภัย (ป้องกัน wrap) แนะนำเป็น:
unsigned long start = millis();
while (millis() - start < sampleWindowMs) {
// อ่านตัวอย่าง
}
-การลบแบบ unsigned ช่วยให้เงื่อนไขยังถูกต้องแม้ millis() จะ wrap-around
2.การป้องกันกรณี sampleCount == 0
-ปกติ sampleCount ควร >=1 เพราะ sampleWindowMs > 0 และ loop
จะอ่านอย่างน้อยหนึ่งครั้ง แต่ถ้ามีเหตุการณ์พิเศษ
(เช่น start ใกล้ wrap แล้วเงื่อนไขเผลอข้าม) ให้เช็คก่อนหาร:
if (sampleCount == 0) sampleCount = 1;
3.การป้องกันค่ลบก่อน sqrt
-เนื่องจากความไม่แม่นยำของ float อาจเกิด meanSq - avg*avg
เป็นค่าลบน้อย ๆ ได้ — ควรตรวจและ clamp:
float tmp = meanSq - avg*avg;
if (tmp < 0) tmp = 0;
float rms = sqrt(tmp);
4.เรื่องชนิดตัวแปร (overflow ของ sumSq)
-sumSq เก็บค่ากำลังสองของ ADC (ค่าสูงสุด 1023^2 ≈ 1,046,529)
ถ้า sampleCount มาก ๆ ผลรวมอาจเกินพิสัยของ long (บน AVR long เป็น 32-bit signed) —
ในตัวอย่างปกติ (50 ms) มักปลอดภัย แต่ถ้าขยายหน้าต่างเป็นวินาที ควรใช้ unsigned long
หรือ unsigned long long สำหรับ sumSq เพื่อความปลอดภัย
5.เพิ่มการกรอง anti-aliasing (ถ้าจำเป็น)
-ADC sampling แบบสุ่ม (analogRead) ไม่มี anti-aliasing filter —
ถ้าสนใจรายละเอียดความถี่หรือต้องการลด aliasing ให้ใส่ RC low-pass
ที่ OUT ก่อนเข้า ADC หรือ sample แบบมีตัวกรองซอฟต์แวร์
6.ปรับ precision / reference
-ถ้าต้องการ sensitivity สูงขึ้น ให้ใช้ analogReference(INTERNAL)
(เช่น 1.1V บน Arduino UNO) แต่ต้องมั่นใจว่าสัญญาณไมค์ไม่เกิน
reference นั้น มิฉะนั้นจะเกิด clipping
*/