산업 제조
산업용 사물 인터넷 | 산업자재 | 장비 유지 보수 및 수리 | 산업 프로그래밍 |
home  MfgRobots >> 산업 제조 >  >> Manufacturing Technology >> 제조공정

EasyFFT – 아두이노 프로젝트를 위한 신뢰할 수 있는 FFT 라이브러리

부품 및 소모품

Arduino Nano R31개

앱 및 온라인 서비스

프로젝트 소개

아두이노처럼 연산 능력이 낮은 보드에서 신호의 주파수를 측정하는 것은 어려운 일입니다. 제로 크로싱 방식으로 주파수를 측정할 수도 있지만, 여러 주파수가 혼합된 신호에서는 잘 작동하지 않습니다. FFT(Fast Fourier Transform)는 이런 문제를 해결하지만, 수학적 배경이 없는 메이커에게는 구현이 까다롭습니다.

이 프로젝트는 FFT의 원리 설명보다는 아두이노에서 쉽게 적용할 수 있는 코드 제공에 초점을 맞춥니다. 동영상에서도 같은 과정을 설명합니다. 코드 적용만 원하시면 3단계로 바로 넘어가세요.

더 빠른 속도(3배)가 필요하고 정확도 5% 정도 타협 가능하다면 ApproxFFT 글을 참고하세요.

1단계: 고속 푸리에 변환(FFT)

DFT(이산 푸리에 변환) 연산량을 줄이기 위해 James Cooley와 John Tukey가 개발한 알고리즘으로, 20세기 가장 중요한 알고리즘 중 하나로 꼽힙니다. 신호를 홀수/짝수 시퀀스로 나누어 계산량을 N²에서 NlogN으로 크게 줄입니다.

수학적 배경은 다음 자료를 참조하세요:

  1. FFT 유도
  2. FFT 이해
  3. FFT 설명
  4. 위키피디아

2단계: 코드 설명 – 빠른 사인/코사인

FFT는 사인/코사인 값을 반복 계산합니다. 아두이노 내장 함수는 느려서 64 샘플 기준 처리 시간이 2배 늘어나므로, 0~90° 사인 값을 255 배율로 미리 계산해 byte 배열(sine_data[91])에 저장했습니다. 이렇게 하면 float 대신 byte를 써서 메모리를 1/4로 줄이고 속도도 향상됩니다. 전역 변수로 선언 필수입니다.

피크 주파수 저장용 f_peaks[5] 배열도 전역 변수로 선언합니다. FFT 실행 후 내림차순으로 상위 5개 주파수가 저장됩니다.

byte sine_data[91] = { 
  0, 4, 9, 13, 18, 22, 27, 31, 35, 40, 44, 49, 53, 57, 62, 66, 70, 75, 79, 83, 
  87, 91, 96, 100, 104, 108, 112, 116, 120, 124, 127, 131, 135, 139, 143, 146, 
  150, 153, 157, 160, 164, 167, 171, 174, 177, 180, 183, 186, 189, 192, 195, 
  198, 201, 204, 206, 209, 211, 214, 216, 219, 221, 223, 225, 227, 229, 231, 
  233, 235, 236, 238, 240, 241, 243, 244, 245, 246, 247, 248, 249, 250, 251, 
  252, 253, 253, 254, 254, 254, 255, 255, 255, 255 
};
float f_peaks[5];

저장된 값으로 임의의 각도 사인/코사인을 계산하는 함수는 부동소수점 나눗셈 1회만 수행해 속도를 높입니다. 64 포인트 기준 8ms, 128 포인트 기준 20ms 단축 효과가 있습니다.

3단계: FFT 함수 설명

FFT는 샘플 수가 2의 거듭제곱(2, 4, 8, 16, 32, 64, 128…)이어야 합니다. 2ⁿ이 아니면 하위 2ⁿ개만 사용합니다(예: 70 → 64). Arduino Nano 메모리 제한으로 128 샘플 이상은 어렵습니다(64 권장).

주요 흐름:

  1. 비트 리버스 순서 생성
  2. 입력 데이터 재배열
  3. FFT 수행
  4. 복소수 진폭 계산
  5. 피크 검출 및 내림차순 정렬
  6. 결과는 f_peaks[]에서 접근

(피크 외 데이터가 필요하면 전역 변수에 복사하도록 코드 수정 필요)

4단계: 코드 테스트

삼각파(샘플링 10Hz, 신호 주파수 1.25Hz)를 입력으로 테스트했습니다. Scilab FFT 결과와 근접하게 일치하며, 저정밀도 사인 테이블 사용으로 약간의 오차는 있습니다. 출력 주파수 빈(bin)은 1.25Hz, 3.75Hz 등으로 나타납니다.

아두이노 Nano 기준 처리 속도:

5단계: 결론

이 FFT 코드는 실시간 애플리케이션에 활용 가능합니다(약 30ms 소요). 해상도는 샘플 수에 의해 제한되며, 아두이노 메모리가 샘플 수를 제한합니다. Arduino Mega 등 고성능 보드를 쓰면 정확도 향상 가능합니다. 문의·제안·수정 사항은 댓글로 남겨주세요.

코드

EasyFFT (Arduino)

/* 
// 예제 데이터:
int data[64]={14, 30, 35, 34, 34, 40, 46, 45, 30, 4, -26, -48, -55, -49, -37, -28, -24, -22, -13, 6, 32, 55, 65, 57, 38, 17, 1, -6, -11, -19, -34, -51, -61, -56, -35, -7, 18, 32, 35, 34, 35, 41, 46, 43, 26, -2, -31, -50, -55, -47, -35, -27, -24, -21, -10, 11, 37, 58, 64, 55, 34, 13, -1, -7};
*/
//---------------------------------------------------------------------------
// 전역 변수: 프로그램 최상단에 붙여넣기
byte sine_data[91] = { 
  0, 4, 9, 13, 18, 22, 27, 31, 35, 40, 44, 49, 53, 57, 62, 66, 70, 75, 79, 83, 
  87, 91, 96, 100, 104, 108, 112, 116, 120, 124, 127, 131, 135, 139, 143, 146, 
  150, 153, 157, 160, 164, 167, 171, 174, 177, 180, 183, 186, 189, 192, 195, 
  198, 201, 204, 206, 209, 211, 214, 216, 219, 221, 223, 225, 227, 229, 231, 
  233, 235, 236, 238, 240, 241, 243, 244, 245, 246, 247, 248, 249, 250, 251, 
  252, 253, 253, 254, 254, 254, 255, 255, 255, 255 
};
float f_peaks[5]; // 내림차순 상위 5개 주파수 피크
//---------------------------------------------------------------------------

void setup() { Serial.begin(250000); }
void loop() {
  /* 예제 사용법
  FFT(data, 64, 100); // 64 샘플, 100Hz 샘플링
  Serial.println(f_peaks[0]);
  Serial.println(f_peaks[1]);
  delay(99999);
  */
}

//----------------------------- FFT 함수 ------------------------------//
float FFT(int in[], int N, float Frequency) {
  /* 아두이노용 FFT
     설정: sine_data[91]를 전역 변수로 선언, 이 함수를 프로그램 끝에 붙여넣기
     매개변수: 1. in[] 데이터 배열, 2. N 샘플 수(2,4,8,16,32,64,128… 권장), 3. Frequency 샘플링 주파수(Hz)
     샘플 수가 2의 거듭제곱이 아니면 하위 2ⁿ으로 자름.
     Arduino Nano는 메모리 제한으로 128 샘플 초과 불가(64 권장)
     작성자: ABHILASH, 연락처: abhilashpatel121@gmail.com
     문서: https://www.instructables.com/member/abhilash_patel/instructables/
     2021/03/02: N 타입을 float→int로 변경(≥256 샘플 대응)
  */
  unsigned int data[13] = {1,2,4,8,16,32,64,128,256,512,1024,2048};
  int a, c1, f, o, x;
  a = N;
  for (int i = 0; i < 12; i++) { if (data[i] <= a) o = i; } // 레벨 계산

  int in_ps[data[o]] = {};       // 비트 리버스용 시퀀싱 입력
  float out_r[data[o]] = {};     // 변환 실수부
  float out_im[data[o]] = {};    // 변환 허수부

  x = 0;
  for (int b = 0; b < o; b++) { // 비트 리버스 순서 생성
    c1 = data[b];
    f = data[o] / (c1 + c1);
    for (int j = 0; j < c1; j++) {
      x = x + 1;
      in_ps[x] = in_ps[j] + f;
    }
  }
  for (int i = 0; i < data[o]; i++) { // 비트 리버스 순서로 입력 정렬
    if (in_ps[i] < a) out_r[i] = in[in_ps[i]];
    else out_r[i] = in[in_ps[i] - a];
  }

  int i10, i11, n1;
  float e, c, s, tr, ti;
  for (int i = 0; i < o; i++) { // FFT 수행
    i10 = data[i];
    i11 = data[o] / data[i + 1];
    e = 360.0 / data[i + 1];
    e = -e;
    n1 = 0;
    for (int j = 0; j < i10; j++) {
      c = cosine(e * j);
      s = sine(e * j);
      n1 = j;
      for (int k = 0; k < i11; k++) {
        tr = c * out_r[i10 + n1] - s * out_im[i10 + n1];
        ti = s * out_r[i10 + n1] + c * out_im[i10 + n1];
        out_r[n1 + i10] = out_r[n1] - tr;
        out_r[n1] = out_r[n1] + tr;
        out_im[n1 + i10] = out_im[n1] - ti;
        out_im[n1] = out_im[n1] + ti;
        n1 = n1 + i10 + i10;
      }
    }
  }

  // 진폭 계산 및 주파수 빈 매핑
  for (int i = 0; i < data[o - 1]; i++) {
    out_r[i] = sqrt(out_r[i] * out_r[i] + out_im[i] * out_im[i]); // 속도 위해 sqrt 생략 가능
    out_im[i] = i * Frequency / N;
  }

  // 피크 검출
  x = 0;
  for (int i = 1; i < data[o - 1] - 1; i++) {
    if (out_r[i] > out_r[i - 1] && out_r[i] > out_r[i + 1]) {
      in_ps[x] = i;
      x++;
    }
  }

  // 크기순 정렬
  float s = 0, c = 0;
  for (int i = 0; i < x; i++) {
    for (int j = (int)c; j < x; j++) {
      if (out_r[in_ps[i]] < out_r[in_ps[j]]) {
        s = in_ps[i];
        in_ps[i] = in_ps[j];
        in_ps[j] = (int)s;
      }
    }
    c = c + 1;
  }

  // 전역 f_peaks 배열 업데이트
  for (int i = 0; i < 5; i++) {
    f_peaks[i] = out_im[in_ps[i]];
  }
}

// 빠른 사인 함수 (0~360도)
float sine(int i) {
  int j = i;
  while (j < 0) j += 360;
  while (j > 360) j -= 360;
  float out;
  if (j >= 0 && j < 91) out = sine_data[j];
  else if (j < 181) out = sine_data[180 - j];
  else if (j < 271) out = -sine_data[j - 180];
  else out = -sine_data[360 - j];
  return out / 255.0;
}

// 빠른 코사인 함수
float cosine(int i) {
  int j = i;
  while (j < 0) j += 360;
  while (j > 360) j -= 360;
  float out;
  if (j >= 0 && j < 91) out = sine_data[90 - j];
  else if (j < 181) out = -sine_data[j - 90];
  else if (j < 271) out = -sine_data[270 - j];
  else out = sine_data[j - 270];
  return out / 255.0;
}
//-------------------------------------------------------------------//

제조공정

  1. 푸리에 변환 이해하기: 기초, 응용 및 신호 분해
  2. 아두이노만으로 DTMF 디코더 구축하기 – 추가 IC 불필요
  3. SERENA: TFT LCD 터치스크린 탑재 아두이노 메가 2560 커스텀 알람 시스템
  4. 파이썬 3로 아두이노 우노 제어하기 – 시리얼 통신 LED 제어 기초
  5. 학교용 스마트 체온 모니터링 시스템: 정확하고 실시간이며 안전한
  6. 아두이노용 효율적인 8비트 IO 포트 라이브러리 – 디지털 제어 확장
  7. 아두이노 64키 프로토타이핑 키보드 매트릭스 - 시프트 레지스터로 핀 절약하기
  8. 아두이노용 절연 아날로그 입력 회로 만들기
  9. 아두이노 프로젝트를 위한 스마트 실내 내비게이션 로봇 키트
  10. 유압 펌프 HS 코드: 분류 체계 및 활용 가이드