본문 바로가기
프로세싱+아두이노+안드로이드

아두이노-푸리에 변환 (FFT)과 활용

by YJHTPII 2022. 5. 14.
반응형

https://geronimob.tistory.com/27?category=145813 

 

아두이노-푸리에 변환 (FFT)과 활용

이번엔 아두이노를 이용한 간단한 신호처리에 대해 알아보겠습니다. 보통 아두이노를 사용하게 되면, Analog Input Digital Input / Output 이 기능들을 주로 사용하게 되고, 보통 아두이노를 사용하시는

geronimob.tistory.com

 

 

이번엔 아두이노를 이용한 간단한 신호처리에 대해 알아보겠습니다.

 

보통 아두이노를 사용하게 되면, 

 

Analog Input

Digital Input / Output 

 

이 기능들을 주로 사용하게 되고,

보통 아두이노를 사용하시는 분들을 보면

 

위 사진처럼

 

아두이노에서 받아온 데이터(Raw Data)는 통신(시리얼통신 or SPI 통신) 을 통해 PC로 전송하게 되죠.

 

전송된 데이터는 PC상에서 연산이 되어서 다시 아두이노로 데이터(Processed Data)를 전송하게되고,

아두이노는 이에 따라 반응하게 됩니다.

 

하지만 이런 구조의 주된 문제점은 아무래도,

시스템의 구조가 커질 수 밖에 없다는 것이죠

 

아두이노와 센서만 있는다고 되는게 아니라, pc까지 있어야 하니 말이죠

 

이런 문제를 해결해 주는 몇몇 보드들이 바로 라즈베리파이, 비글본 블랙, 아두이노 윤, 등등

리눅스(Linux) OS 가 설치되어있는 보드들이랍니다.

 

이들은 보드 하나에 아두이노와 리눅스가 붙어있어서 상당히 간편하게 사용할 수 있지만, 

다루기가 쉽지않아서 처음 이용하시는 분들에게는 쉽지 않습니다.

 

그래서 이번시간엔 아두이노만 이용해서

데이터 입출력 및 프로세싱을 해보도록 하겠습니다~

 

#1 푸리에 변환 (Fourier Transform)

 

먼저 신호분석에서 가장 흔하게 사용하는것이

푸리에 변환 (Fourier Transform) 일겁니다.

푸리에변환은 시간영역의 신호를 주파수 영역의 신호로 변환 시켜주는 함수에요.

 

좀 더 구체적으로 설명하기위해 '소리'에 대해 봅시다.

소리신호를 녹음해서 보면 빨간 박스에서 보이는것 처럼 지글지글한 신호로 보이죠?

 

근데 저 신호만 봐서는 이 소리신호의 음 높이가 '도' 인지, '파' 인지 알수가 없죠.

근데 자세히 보면 빨간 박스의 그래프는 x축이 시간(time)이랍니다.

즉, 시간에 따라 신호가 어떻게 흘러가는지만 볼 수 있을 뿐,

음의 높낮이 정보인 주파수(Hz)는 알 수가 없습니다.

 

이를 알 수 있게 해주는 것이 바로 푸리에 변환.

 

빨간 박스의 정보를 푸리에 변환을 하게되면

파란 박스에 있는 정보처럼 이상한 형태의 신호가 나옵니다.

하지만 파란 박스를 자세히 보면 x축이 시간이 아닌 주파수(Frequency)가 됩니다.

 

즉, 주파수 정보인 음의 높낮이를 파악할 수 있다는 거죠.

 

그럼 다른 사람이 같은 음을 내더라도 목소리가 다른건 어떻게 알 수 있을까요?

목소리는 '음색'이라 표현하기도 하며, 푸리에 변환을 하게되면 주파수 영역에서 다른 형태를 보입니다.

 

위 사진은 4옥타브 도 (C4, 261.6Hz)를 각 악기별로 연주할 때 나타나는 신호입니다.

[피아노(a), 트럼펫(b), 바이올린(c), 플룻(d)]

 

보시다싶이 주파수별로 나타나는 형태가 조금씩 다르죠?

저런 특성들을 이용해 음색을 분별 할 수도 있답니다.

 

아무튼 푸리에 변환은 기존 신호에 숨어있던 정보들을 뽑아낼 수 있는 기능을 가졌지만,

연산량이 상당하기때문에 일반 컴퓨터로도 큰 데이터의 푸리에변환은 쉽지않답니다.

 

 

#2 아두이노를 이용한 푸리에 변환

 

 

자 이제 이러한 푸리에변환을 빠르게는 못하지만,

아두이노를 이용해 신호처리의 흉내를 내보도록 하겠습니다.

 

사실 컴퓨터로 푸리에변환을 할때에는 수식적으로 적분을 계산하는 것이 아니라,

다른 방법을 통해서 간접적으로 빠르게 구하는 Fast Fourier Transform(FFT) 를 주로 사용한답니다.

 

아두이노에서는 공식적으로 FFT함수가 따로 없기때문에, 사용자가 만든 함수를 사용합니다.

 

제가 알려드릴 함수는 크게 두가지 입니다.

 

1. C언어로 구성된 라이브러리 (쉬움, 하지만 느림)

 arduinoFFT-master.zip

 

 

  1. /* Arduino Code */
  2.  
  3. #include "arduinoFFT.h"
     
    arduinoFFT FFT = arduinoFFT(); /* Create FFT object */
    /*
    These values can be changed in order to evaluate the functions
    */
    const uint16_t samples = 128; //This value MUST ALWAYS be a power of 2
    double signalFrequency = 1000;
    double samplingFrequency = 5000;
    uint8_t amplitude = 100;
    /*
    These are the input and output vectors
    Input vectors receive computed results from FFT
    */
    double vReal[samples];
    double vImag[samples];
     
    #define SCL_INDEX 0x00
    #define SCL_TIME 0x01
    #define SCL_FREQUENCY 0x02
     
    #define Theta 6.2831 //2*Pi
     
    void setup()
    {
      Serial.begin(115200);
    //  Serial.println("Ready");
    }
     
    void loop()
    {
      for (uint8_t i = 0; i < samples; i++)
      {
        vReal[i] = analogRead(A0);
        delayMicroseconds(100);
        vImag[i] = 0;
      }
      FFT.Windowing(vReal, samples, FFT_WIN_TYP_HAMMING, FFT_FORWARD); /* Weigh data */
      FFT.Compute(vReal, vImag, samples, FFT_FORWARD); /* Compute FFT */
      FFT.ComplexToMagnitude(vReal, vImag, samples); /* Compute magnitudes */
      PrintVector(vReal, (samples >> 1), SCL_FREQUENCY);
    }
     
    void PrintVector(double *vData, uint8_t bufferSize, uint8_t scaleType)
    {
      for (uint16_t i = 2; i < bufferSize; i++)
      {
        uint8_t val_temp = map(vData[i],0,1000,0,255);
        Serial.write(val_temp);
      }
      Serial.write('\n');
    }

 

  1. /* Processing code */
     
    import processing.serial.*;
     
    Serial myPort;        // The serial port
     
    int[] val = new int[64];
     
    void setup () {
      // set the window size:
      size(1024, 1024);
     
      String portName = Serial.list()[2];  // Set your port correctly
      myPort = new Serial(this, portName, 115200);
      
      // don't generate a serialEvent() unless you get a newline character:
      myPort.bufferUntil('\n');
     
      // set inital background:
      background(255);
      rect(0, 0, 1024, 1024);
     
      // set stroke line color to red.
      stroke(255,0,0);
      
      //delay for arduino serial reboot.
      delay(2000);
    }
     
    void draw () {
        //Clear background for re-drawing.
        background(255);
     
        //Draw stroke for each frequency 
        for (int i =0; i<64; i++){
          line(i*16 +1,height,i*16 +1,height-val[i]*4);
        }
    }
     
     
    void serialEvent (Serial myPort) {
      try{
        
          //Create buffer for data saving
          byte[] inByte = new byte[200];
          
          //Put data into buffer
          myPort.readBytesUntil('\n',inByte);
          
          // convert to an int and map to the screen height:
          for(int i = 0; i<64; i++){
           val[i] = int(inByte[i]);      
          }
     
      }catch(Exception e){
        println("Err");
      }
    }

 

 

 

 

 

2. AVR로 구성된 라이브러리(어려움, 하지만 빠름)

 

그리고 아두이노 우노에서만 동작합니다!!

 

아무래도 레지스터를 건드리다보니, 물리적으로 칩이 우노에 사용되는 ATMEGA328P이 아니면

동작이 안된답니다...!

 

 ArduinoFFT3.zip

 

 

  1. /* Code for Arduino */
  2. /*
    fft_adc_serial.pde
    guest openmusiclabs.com 7.7.14
    example sketch for testing the fft library.
    it takes in data on ADC0 (Analog0) and processes them
    with the fft. the data is sent out over the serial
    port at 115.2kb.
    */
     
    #define LOG_OUT 1 // use the log output function
    #define FFT_N 256 // set to 256 point fft
     
    #include <FFT.h> // include the library
    #include <avr/io.h>
    #include <avr/interrupt.h>
     
    void setup() {
      Serial.begin(115200); // use the serial port
      TIMSK0 = 0; // turn off timer0 for lower jitter
      ADCSRA = 0xe5; // set the adc to free running mode
      ADMUX = 0x40; // use adc0
      DIDR0 = 0x01; // turn off the digital input for adc0
    }
     
    void loop() {
      while(1) { // reduces jitter
        cli();  // UDRE interrupt slows this way down on arduino1.0
        for (int i = 0 ; i < 512 ; i += 2) { // save 256 samples
          while(!(ADCSRA & 0x10)); // wait for adc to be ready
          ADCSRA = 0xf5; // restart adc
          byte m = ADCL; // fetch adc data
          byte j = ADCH;
          int k = (j << 8) | m; // form into an int
          k -= 0x0200; // form into a signed int
          k <<= 6; // form into a 16b signed int
          fft_input[i] = k; // put real data into even bins
          fft_input[i+1] = 0; // set odd bins to 0
        }
        fft_window(); // window the data for better frequency response
        fft_reorder(); // reorder the data before doing the fft
        fft_run(); // process the data in the fft
        fft_mag_log(); // take the output of the fft
        sei();
        Serial.write(fft_log_out, FFT_N/2); // send out the data
        Serial.write("\n");
      }
    }
  3.  

 

 

  1. /* Processing code */
    import processing.serial.*;
     
    Serial myPort;        // The serial port
     
    int[] val = new int[128];
     
    void setup () {
      // set the window size:
      size(1024, 1024);
     
      String portName = Serial.list()[2];
      myPort = new Serial(this, portName, 115200);
      
      // don't generate a serialEvent() unless you get a newline character:
      myPort.bufferUntil('\n');
     
      // set inital background:
      background(255);
      rect(0, 0, 1024, 1024);
      stroke(255,0,0);
      noLoop();
      delay(2000);
    }
     
    void draw () {
        background(255);
     
        for (int i =0; i<128; i++){
          line(i*8 +1,height,i*8 +1,height-val[i]*4);
        }
    }
     
     
    void serialEvent (Serial myPort) {
      try{
          byte[] inByte = new byte[200];
          
          myPort.readBytesUntil('\n',inByte);
          
            // convert to an int and map to the screen height:
            for(int i = 0; i<128; i++){
             val[i] = int(inByte[i]);      
            }
            redraw();
     
      }catch(Exception e){
        println("Err");
      }
    }

프로세싱코드 컴파일시

ArrayIndexOutofBounds 오류 발생시

[2] 를 [0]으로 변경

설명: 가용 시리얼 포트 리스트, 첫번째 포트가 아두이노 연결 포트이므로 0 으로 세팅, 또는 COM3처럼 포트를 강제할당

 

 

각 구성을 보시고 본인 쓰기에 편하신 라이브러리로 쓰시면 됩니다!

 

아래 이미지는 프로세싱을 통해 모니터링한 결과입니다.

 

각각 코드를 돌려보면,

 

1번 결과

 

 

2번 결과

 

 

데이터의 갯수와 샘플링 속도차이가 조금 있긴하지만, 첫번째 코드는 깔끔한 반면 느리고 신호가 불안정합니다.

 

하지만 두번째 경우, 신호가 깔끔하고 안정적이지만, 코드가 복잡합니다.

 

어떤걸 쓰게 될지는 센서의 성능에 따라 결정하시는게 좋을듯 합니다ㅎㅎ



반응형

댓글