กลับไปหน้ารวมไฟล์
approxfft-fastest-fft-function-for-arduino-f1b6ba.md

FFT หรือ Fast Fourier transform เป็นหนึ่งในเครื่องมือสำคัญที่สุดในโลกของการประมวลผลสัญญาณ ช่วยให้เราวิเคราะห์องค์ประกอบความถี่จากสัญญาณดิบในโดเมนเวลาได้อย่างรวดเร็ว ใช้ตรวจจับความถี่ได้เร็วดีเลย แต่กระบวนการนี้มันหนักมากกก กินทรัพยากรทั้งพลังประมวลผลและหน่วยความจำ (RAM) โดยเฉพาะบนบอร์ดไมโครคอนโทรลเลอร์ทรัพยากรจำกัดอย่าง Arduino ทำให้ใช้งานยากสำหรับงานแบบเรียลไทม์ที่ต้องการผล FFT หลายครั้งต่อวินาที

ในบทสอนนี้ เราจะพาน้องไปรู้จักกับโปรแกรมที่เร็วที่สุดสำหรับทำ FFT บน Arduino พี่ตั้งชื่อมันว่า ApproxFFT เพราะมีการประมาณค่า (approximation) ไว้หลายขั้นตอนมากก แต่ด้วยการประนีประนอมความแม่นยำเพียงนิดเดียว เราก็ได้ความเร็วเพิ่มขึ้นมาประมาณ 3 เท่าเลยทีเดียว

ถ้าน้องอยากรู้วิธีใช้แบบตรงๆ ข้ามไปที่ข้อ 5 ได้เลยจ้า

โครงการนี้พี่อธิบายไว้ในวิดีโอความยาวครึ่งชั่วโมง น้องจะเลือกดูวิดีโอหรืออ่านบทสอนนี้ก็ได้ตามสะดวก

Video Explaining The project

ก่อนหน้านี้พี่เคยเตรียมโค้ดสองตัวสำหรับงานเดียวกันไว้: EasyFFT: โค้ดตัวนี้ให้ผลลัพธ์ FFT ที่แม่นยำ แต่มันค่อนข้างช้าและกินหน่วยความจำสูง แนะนำให้อ่านบทสอนตัวนี้เพื่อทำความเข้าใจพื้นฐาน FFT ก่อน QuickFFT: โค้ดตัวนี้ให้ผลลัพธ์เร็วมากกก แต่ค่าอัมปลิจูด (amplitude) เบี่ยงเบนไปไกลและใช้กับงานส่วนใหญ่ไม่ได้ มันยังให้พีค (peak) หลายจุดซึ่งอาจทำให้ผลลัพธ์เข้าใจผิดได้

1: ความจำเป็นของฟังก์ชัน ApproxFFT

ถ้าน้องดูรูปที่แนบมา จะเห็นชัดว่า QuickFFT มีข้อได้เปรียบสำคัญเหนือวิธีดั้งเดิม (คำนวณความเร็วด้วยฟังก์ชัน sine/cosine ในตัวของ EasyFFT) มันเร็วเกือบ 4 เท่า แต่ขาดความแม่นยำ อย่างที่เห็นในรูปที่สอง ค่าอัมปลิจูดเบี่ยงเบนไปไกล สาเหตุหลักคือเราใช้คลื่นสี่เหลี่ยม (ประมาณเป็นคลื่น sine !!!) ซึ่งประกอบด้วยหลายความถี่ (ฮาร์มอนิก) ทำให้มีหลายคลื่นปรากฏในผลลัพธ์ สร้างความยุ่งยากสำหรับการประยุกต์ใช้งานต่างๆ

ตรงนี้เราเคยประมาณคลื่น sine/cosine เป็นคลื่นสี่เหลี่ยม ถ้าเราใช้การประมาณที่ดีกว่านี้สำหรับคลื่นเหล่านี้ เราก็จะเข้าใกล้ผลลัพธ์ที่แท้จริงมากขึ้น ในโค้ดนี้เราได้พิจารณาการประมาณที่ดีกว่าสำหรับคลื่นเหล่านี้ และเรายังมีตัวเลือกในการควบคุมความแม่นยำของคลื่นเหล่านี้ด้วย การปรับแต่งการตั้งค่านี้ทำให้เราสามารถเล่นกับความสัมพันธ์ระหว่างความแม่นยำ/ความเร็วได้หลายแบบเลย สู้งานนะน้อง!

2: ฟังก์ชันไซน์และโคไซน์แบบเร็วปึ๊ก

ถ้าน้องย้อนกลับไปดูฟังก์ชัน EasyFFT ในตอนก่อนหน้า เรามีสองตัวเลือกให้ใช้ ตัวนึงคือแบบแม่นยำ ใช้ฟังก์ชันไซน์/โคไซน์ในตัวของ Arduino ซึ่งแม่นแต่ช้าครับ อีกวิธีคือใช้ตารางค้นหา (lookup table) ซึ่งเร็วขึ้นนิดหน่อยแต่ความแม่นยำก็ลดลงไปบ้าง

ในวิธีนี้เราใช้แนวคิดที่ต่างออกไปโดยสิ้นเชิงในการคำนวณค่า ซึ่งทำให้มันเร็วขึ้นได้ประมาณ 10-12 เท่าเมื่อเทียบกับวิธีปกติครับ ฟังก์ชันพวกนี้ใช้การดำเนินการบิตชิฟต์ (bitshift operation) อย่างหนักสำหรับการคูณและการหาร (แบบประมาณ) ซึ่งการดำเนินการแบบนี้เร็วกว่าการคูณหารปกติแบบสุดๆ แต่มันก็อาจทำให้ความแม่นยำลดลง ซึ่งเราต้องจัดการให้ดี

ในฟังก์ชัน FFT ทั่วไป ส่วนที่กินเวลามากที่สุดคือการคูณเลขทศนิยม (Float) ด้วยค่าไซน์/โคไซน์ของมุมต่างๆ ในขั้นตอนที่เรียกว่า "การคำนวณผีเสื้อ (Butterfly Calculation)" ตามโค้ดด้านล่าง ทั้งสองกระบวนการนี้ช้าและใช้เวลามาก

tr=c*out_r[i10+n1]-s*out_im[i10+n1];
ti=s*out_r[i10+n1]+c*out_im[i10+n1];  // c is cosine and s is sine of some value

ใน ApproxFFT นี้ พี่ใช้วิธีที่ต่างออกไปซึ่งเร็วขึ้น 10-12 เท่า โดยใช้ การดำเนินการบิตชิฟต์ (Bitshift Operations) ร่วมกับอัลกอริทึมการค้นหาแบบทวิภาค (Binary Search) เพื่อหาค่า $A \cdot \sin(\theta)$ โดยตรง โดยไม่ต้องทำการคูณเลขทศนิยมเลย

หลักการทำงานของ Binary Search Sine: เราเก็บตาราง isin_data ซึ่งแมปมุมกับค่าไซน์ในรูปแบบจำนวนเต็ม (0-1024 แทน 0-360 องศา) และใช้วิธีการแบ่งครึ่งช่วง (halving range) เพื่อหาค่าแอมพลิจูด ที่นี่เราใช้สิ่งที่คล้ายกับอัลกอริทึมการค้นหาแบบทวิภาค มันจะให้ผลลัพธ์ประมาณสำหรับ A*sin(θ) โดยตรง และเราไม่ต้องทำการคูณ ในกราฟแรกจะแสดงพล็อตของ ArcsinX เทียบกับ X พล็อตที่คล้ายกันนี้แหละที่ถูกเก็บเป็นตารางใน Arduino โดยที่มุมถูกแมปจาก 0-1024 ซึ่งเทียบเท่ากับ 0-360 องศา ค่า arcsine ก็ถูกพิจารณาเป็นผลคูณของ 128 ด้วย

นี่คือขั้นตอนการคำนวณ (เราจะมองข้ามค่าลบและพิจารณาแค่ 0-90 องศา เช่น 500*sin 50 (หน่วยองศา)):

  1. สำหรับค่าใดๆ (เช่น 500) ผลคูณของไซน์สามารถเป็นได้ตั้งแต่ 0 (ที่ 0 องศา) ถึงค่านั้น (ที่ 90 องศา) (เช่น 0-500) เราจะตรวจสอบค่ามุมที่จุดกึ่งกลาง เราจะหารค่าอินพุต (500) ลงครึ่งหนึ่งและตรวจสอบมุมสำหรับ 0.5 จากนั้นเราสามารถตรวจสอบได้ว่าผลลัพธ์จะอยู่ระหว่าง 0-จุดกึ่งกลาง หรือ จุดกึ่งกลาง-ค่าสูงสุด (ในกรณีของเรา มุมสำหรับ 0.5 จะเป็น 30 องศาซึ่งน้อยกว่า 50 ดังนั้นเราสามารถสรุปได้ว่าผลลัพธ์จะอยู่ที่ไหนสักแห่งระหว่าง 250-500) ณ จุดนี้ เราได้ลดขนาดของช่วงที่เป็นไปได้ลงครึ่งหนึ่งแล้ว (จากเดิม 0-500 เหลือ 250-500)

  2. ในช่วงที่กำหนดใหม่ เราจะทำขั้นตอนที่คล้ายกันซ้ำอีกครั้ง ในตัวอย่างของเรา จุดกึ่งกลางจะอยู่ที่ 375 ซึ่งเราจะตรวจสอบมุมสำหรับ .75 นั่นจะได้ 48 องศา ดังนั้นคำตอบของเราจะอยู่ระหว่าง 375 ถึง 500

ต้องทำขั้นตอนที่คล้ายกันนี้ซ้ำหลายครั้งเพื่อให้ได้ผลลัพธ์ที่แม่นยำ อย่างที่แสดงในภาพที่สอง (กราฟ) ยิ่งเราเพิ่มระดับ (iteration) มากเท่าไหร่ ผลลัพธ์ก็จะยิ่งใกล้เคียงกับค่าจริงมากขึ้น โดยที่ไม่ต้องพึ่งพา FPU (Floating Point Unit) ของ Arduino เลยสักนิด

ที่นี่เราสามารถดำเนินการทั้งหมดได้ด้วยแค่การบิตชิฟต์ และเราก็กำจัดขั้นตอนการคูณเลขทศนิยมออกไปได้แล้ว งานนี้จัดไปวัยรุ่น! สู้งานนะน้อง

3: Fast Root of Squared Sum (FastRSS)

ฟังก์ชันนี้ทำการประมาณค่า RSS ครับ มันไม่ได้ช่วยให้โปรแกรมเร็วขึ้นแบบสุดๆ แต่ก็ประหยัดเวลาไปได้เป็นมิลลิวินาทีเหมือนกัน ดีกว่าไม่มีเนอะ

หลังจากได้ค่า Real กับ Imaginary มาจาก FFT แล้ว ขั้นตอนต่อไปคือหาขนาด (Magnitude) ของสัญญาณด้วยสูตร $\sqrt{R^2 + I^2}$ ซึ่งถ้าใช้ฟังก์ชัน sqrt() มาตรฐานเนี่ย มันช้ามากเลยครับพี่

กราฟที่แนบมาแสดงค่าความผิดพลาด ถ้าเราสมมติว่าคำตอบของรากที่สองของผลบวกกำลังสอง เท่ากับค่าที่มากกว่าเลย ความผิดพลาดจะลดลงเมื่ออัตราส่วนระหว่างค่าทั้งสองเพิ่มขึ้น ผมเลยสร้างฟังก์ชัน fastRSS ขึ้นมาเพื่อประมาณค่านี้โดยใช้ความสัมพันธ์ของอัตราส่วนระหว่างค่ามากและค่าน้อย ถ้าค่าหนึ่งมากกว่าอีกค่ามากๆ เราก็ใช้ค่าที่มากกว่าเป็นคำตอบได้เลยโดยมีความผิดพลาดนิดเดียว แต่ถ้าค่าทั้งสองใกล้เคียงกัน เราจะใช้ตาราง RSSdata และการปรับสเกลมาช่วยชดเชยค่าให้ใกล้เคียงความจริงมากที่สุด ซึ่งช่วยประหยัดเวลาไปได้หลายมิลลิวินาทีเลย ในโค้ดนี้ ถ้าอัตราส่วนเกินค่าที่กำหนดไว้ มันก็จะตอบเป็นค่าที่มากกว่าเลยเพื่อประหยัดเวลา แต่ถ้าอัตราส่วนต่ำกว่านั้น ก็จะใช้การปรับสเกลที่กำหนดไว้ล่วงหน้าตามค่าในตาราง RSSdata นั่นแหละ

4: โฟลว์การคำนวณโดยรวม:

โฟลว์การคำนวณทั้งหมดจะเป็นตามนี้ครับ โปรแกรมจะทำงานตามลำดับขั้นตอนนี้เพื่อประสิทธิภาพสูงสุด:

  1. ปรับสเกลข้อมูลอินพุตให้อยู่ในช่วง +512 ถึง -512: ขั้นตอนนี้สำคัญเพื่อรักษาความแม่นยำของข้อมูล เพราะเราใช้การดำเนินการบิตชิฟต์ ถ้าเป็นเลขคี่จะมีความแม่นยำหาย ยิ่งตัวเลขใหญ่ การสูญเสียน้อยลง
  2. สร้างลำดับบิตกลับ (Bit reversed order)
  3. เรียงลำดับข้อมูลอินพุตตามลำดับบิตกลับที่สร้างไว้
  4. ทำการแปลงฟูเรียร์ (Frequency transform) โดยใช้ fast sine และ fast cosine ในจุดที่จำเป็น หลังจากทุกลูป จะมีการตรวจสอบว่าค่าแอมพลิจูดเกิน 15000 หรือไม่ ถ้าเกิน ทั้งอาร์เรย์ Real และ Imaginary จะถูกหารด้วย 2 ทันที (เพื่อป้องกันการล้นของ integer 16-bit) และบันทึกค่าสเกลที่ใช้หารไว้
  5. คำนวณรากที่สองของผลบวกกำลังสองโดยใช้ฟังก์ชัน FastRSS
  6. ตรวจจับค่าสูงสุดและส่งคืนเป็นผลลัพธ์

5: วิธีใช้งาน

  1. ต้องประกาศข้อมูลลุคอัพ (Lookup data) เป็นตัวแปรโกลบอล แค่ก็อปปี้ข้อมูลด้านล่างไปวางไว้ด้านบนสุดของโค้ดเลย
//---------------------------------lookup data------------------------------------//
byte isin_data[128]=
{0,  1,   3,   4,   5,   6,   8,   9,   10,  11,  13,  14,  15,  17,  18,  19,  20,
22,  23,  24,  26,  27,  28,  29,  31,  32,  33,  35,  36,  37,  39,  40,  41,  42,
44,  45,  46,  48,  49,  50,  52,  53,  54,  56,  57,  59,  60,  61,  63,  64,  65,
67,  68,  70,  71,  72,  74,  75,  77,  78,  80,  81,  82,  84,  85,  87,  88,  90,
91,  93,  94,  96,  97,  99,  100, 102, 104, 105, 107, 108, 110, 112, 113, 115, 117,
118, 120, 122, 124, 125, 127, 129, 131, 133, 134, 136, 138, 140, 142, 144, 146, 148,
150, 152, 155, 157, 159, 161, 164, 166, 169, 171, 174, 176, 179, 182, 185, 188, 191,
195, 198, 202, 206, 210, 215, 221, 227, 236};
unsigned int Pow2[14]={1,2,4,8,16,32,64,128,256,512,1024,2048,4096};
byte RSSdata[20]={7,6,6,5,5,5,4,4,4,4,3,3,3,3,3,3,3,2,2,2};
//---------------------------------------------------------------------------------//
  1. ฟังก์ชัน ApproxFFT, fast_sine, fast_cosine และ fastRSS ต้องก็อปปี้ไปวางไว้ท้ายสุดของโค้ด

  2. วิธีเรียกใช้ฟังก์ชัน:

float f=Approx_FFT(data,sample,sampling_rate);

ฟังก์ชันนี้จะคืนค่าความถี่ที่มีแอมพลิจูดสูงสุดเป็นค่าเริ่มต้น เหมือนกับฟังก์ชัน EasyFFT และ QuickFFT เลย

  • อาร์กิวเมนต์แรกคืออาร์เรย์ของข้อมูลที่ต้องการทำ FFT

  • อาร์กิวเมนต์ที่สองคือจำนวนตัวอย่าง (sample) ที่ควรเป็น 2^n เช่น 2, 4, 8, 16, 32, 64, 128,... โดยจำนวนตัวอย่างสูงสุดจะถูกจำกัดโดยหน่วยความจำที่มีอยู่

  • อินพุตตัวที่สามคืออัตราการสุ่มตัวอย่าง (sampling rate)

  1. การตั้งค่าความแม่นยำ (Accuracy setting):

ค่าตัวนี้ปรับได้ตั้งแต่ 1 ถึง 7 ยิ่งตัวเลขสูงยิ่งแม่นยำมากขึ้น การเพิ่มความแม่นยำจะทำให้คลื่นไซน์ประมาณ (approximate sine wave) ของเราพอดีกับค่าจริงมากขึ้น โดยค่าเริ่มต้น (default) จะตั้งไว้ที่ 5 ซึ่งในกรณีส่วนใหญ่ค่านี้ก็ใช้ได้ดีอยู่แล้ว ผลลัพธ์และความเร็วที่อ้างอิงทั้งหมดก็มาจากการตั้งค่านี้แหละ ความเร็วและความแม่นยำสามารถปรับแต่งได้ด้วยการเปลี่ยนค่านี้

int fast_sine(int Amp, int th)
{
int temp3,m1,m2;
byte temp1,temp2, test,quad,accuracy;
accuracy=5;    // set it value from 1 to 7, where 7 being most accurate but slowest
               // accuracy value of 5 recommended for typical applicaiton
while(th>1024){th=th-1024;}   // here 1024 = 2*pi or 360 deg
while(th<0){th=th+1024;}
.
.
.
  1. โค้ดนี้สามารถดัดแปลงเพื่อแสดง (และใช้) ผลลัพธ์ดิบ (Raw output) หรือแอมพลิจูดของความถี่ต่างๆ ได้ โดยผลลัพธ์จะแสดงเป็นผลคูณของ 2^n เนื่องจากตัวแปรจำนวนเต็ม (integer) เก็บค่าได้สูงสุดแค่ +-32000 ผลลัพธ์เลยถูกแสดงในรูปแบบผลคูณแทน

6: สรุป

การทดสอบทั้งหมดที่เห็นในภาพด้านบนทำบน Arduino mega ด้วยระดับความแม่นยำที่ 5 มาดูกันว่าผลทดสอบเจ๋งแค่ไหน:

จากการทดสอบบน Arduino Mega ที่ระดับความแม่นยำ 5 พบว่า:

  • ความเร็วในการประมวลผล เร็วกว่า FFT ทั่วไปมากกว่า 3 เท่า งานไวปรื๊ด!
  • การใช้หน่วยความจำลดลงเกือบ 50% ประหยัด RAM ไปได้เยอะ
  • ผลลัพธ์ใกล้เคียงกับค่าจริงมาก (คลาดเคลื่อนต่ำ) เหมาะจะเอาไปใช้ในโปรเจคฝังตัว (embedded) จริงๆ

หวังว่าโค้ดนี้จะมีประโยชน์กับโปรเจคของน้องๆ นะครับ ถ้ามีคำถามหรือข้อเสนอแนะอะไร ก็คอมเมนต์มาคุยกันได้เลย

ข้อมูล Frontmatter ดั้งเดิม

apps:
  - "1x Arduino IDE"
author: "abhilashpatel121"
category: "Lab Stuff"
components:
  - "1x Arduino Mega 2560"
description: "ฟังก์ชันเทพๆ ตัวนี้ทำงาน FFT ได้เร็วแรงจัดไป แถมยังแม่นยำแบบงานง่ายแต่หล่อ ไม่ทำให้โปรเจควัยรุ่นพัง!"
difficulty: "Intermediate"
documentationLinks: []
downloadableFiles:
  - "https://projects.arduinocontent.cc/0d7a396b-4877-4297-8d83-ef4b7525893c.ino"
  - "https://projects.arduinocontent.cc/0d7a396b-4877-4297-8d83-ef4b7525893c.ino"
encryptedPayload: "U2FsdGVkX1+rjgonA7Sp/xfGaqVxMcSFTRLQKgrX7NSAMC0JCqSxZcnPvXhO0ZIFWd94HNvo+fEvpHuAfkjQkJyiukPQxHavVjFCXjVy0aQ="
heroImage: "https://cdn.jsdelivr.net/gh/bigboxthailand/arduino-assets@main/images/projects/approxfft-fastest-fft-function-for-arduino-f1b6ba_cover.jpg"
lang: "en"
likes: 1
passwordHash: "fbc5fffe3ec3e3969c5ebd470c55155c3fdfba34c0dcea3132cf29258ef04b1c"
price: 2450
seoDescription: "ApproxFFT: The fastest FFT Function for Arduino projects, balancing high speed and accuracy."
tags:
  - "fft"
  - "frequency"
title: "ApproxFFT: ฟังก์ชัน FFT ไวสุดตึง! สำหรับ Arduino"
tools: []
videoLinks:
  - "https://www.youtube.com/embed/IWr6ZUK_aps"
views: 34236