ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 아두이노 오실로스코프
    아두이노 2023. 1. 31. 17:40

    오실로스코프는 나름 고가에 해당하는 계측기기로, 신호의 파형을 관찰하는 데 사용한다. 여러번 측정하여 전압의 평균값을 보여주는 멀티미터와 다르게, 작은 노이즈까지 볼 수 있기때문에 무언가를 개발할 때 있으면 편리하며, 정밀한 회로를 만든다면, 켈리브레이션에 꼭 필요한 장비이다. 밀리세컨드 단위에 시간동안 일어나는 일을 직접 눈으로 보고 이해한 쪽과 그렇지 못한 쪽은 차이가 크다. 저렴하게 만들려고 기획한 것이라 오실로스코프의 모든 기능을 다 넣지는 못하였지만, 그럭저럭 간이로 쓸만하게 만들어 보았다. 객체지향적인 설계를 자세히 보여줄 생각인데, 단순히 인터넷의 자료를 보고 따라하는 단계를 넘는데 도움이 되면 좋겠다.

     

    이번에 OLED를 처음 사용해보았는데, 예제만 잘 보면 사용하는데 크게 어려움이 없었으니 부담없이 따라오면 된다. 다만 글을 읽기전에 포인터가 무엇인지, 클래스가 무엇인지 정도는 알아보고 오면 좋을 것 같다.

     

    회로 설계

    마이크로 컨트롤러 같은 경우 소프트웨어를 설계할 때 하드웨어도 같이 연관지어 생각해야한다.

    가장 기본적으로 파형을 제대로 관찰하기 위해서 데이터 수집 간격을 조절할 필요가 있었고, 화면에 최대, 최소, 중간값, 평균, 수집 간격을 표시할 필요가 있다. 가장 핵심적인 기능이니 이 부분부터 개발한다.

     

    버튼에는 채터링 현상이 있어, 버튼이 눌리는 순간, 눌럿다가 떨어지는 순간 관성에 의해 진동하면서 0,1이 반복되는 진동이 일어난다. 깔끔한 버튼 입력을 위해 디바운스 회로를 적용하였다.

    이미지 출처: https://www.devicemart.co.kr/goods/view?no=12547594

     이 회로에서 슈미트 트리거 부분만 제외하고 사용하였다. 기본적으로 풀업 저항 버튼인데, R2와 C가 R-C 필터로 작용하는데, R-C필터는 캐패시터가 갑자기 전압이 떨어지면 방전하여 전압을 유지하고, 전압이 오르면 충전되어 전압이 오르는 것을 방해한다. 저항은 캐패시터가 충전되는 시간을 조절하여, 저항값이 클수록 충전속도가 느려저 고주파가 통과하기 어려워진다. 또 캐패시턴스가 클수록 충전되는데 오래걸리기 때문에 고주파가 통과하기 어려워진다. 정밀하게 설계한다면 오실로스코프로 채터링때 주파수를 분석해서 R,C 값을 맞추지만, 보통 캐패시터는 10uf 세라믹 케페시터, 저항은 10k옴 이상을 사용한다.

     

    출처 https://m.blog.naver.com/lagrange0115/220659006305

    이때 다이오드는 R2와 폐회로를 형성하여 버튼이 눌러진 순간 캐패시터의 잔류 전하가 저항에서 빨리 소모될 수 있도록 도와주는 역할을 한다. 다이오드가 없다면 다이오드를 빼도 괜찮으며, 간단하게 구성하고 싶다면 10uf 캐패시터만 디지털 입력핀과 그라운드 사이에 연결해주기만 해도 된다.

     

    최종적인 회로 연결

    OLED 연결

    SCL(CLK) 13
    SDA(MOSI) 11
    RES 8
    DC 9
    CS 10(없어서 연결 안함)

    사진처럼 CS핀이 없는 비교적 저렴한 것을 구매하면 SPI 통신의 특징인 1:n 통신은 불가능하다.(SPI 통신 프로토콜에 대해 찾아보면 쉽게 이해할 수 있을 것입니다.)

     

    버튼은 각각 2,3,4와 연결하였다.(기능에 대해서는 아래에서 설명)

     

    코딩

     c 스타일의 절차지향 방식과 c++ 스타일의 객체지향 방식의 개발을 비유하자면, 전자는 기능단위로 함수를 만들어 함수가 실행될 순서를 나열한다면, 후자는 개발이 편해지도록 쓰기 좋은 도구를 만든 뒤, 그 도구를 이용하여 전체적인 프로그램을 만든다고 할 수 있다. 다리를 만드는 것에 비유하자면, c는 현장에서 주물을 만들고 콘크리트를 부어 다리 상판을 만든다면, 후자는 공장에서 다리 상판을 블럭처럼 만든 뒤, 현장에서 블럭을 조립하여 다리를 만드는 것이다.(현재에는 주로 후자의 방식으로 다리를 만든다고 들었다.)

     

    처음 제대로 오실로스코프의 메인 기능이 제대로 작동되는 것을 확인 한 후, 기능추가 및, 화면 크기가 다르더라도 자동으로 맞춰 오실로스코프 화면을 생성하도록 확장하려 하였다. 처음 설계한 코드는 확장에 대한 고려가 되지 않아, 코드를 갈아업었는데, 무거워서 제대로 작동이 되지 않았다. 마지막으로 한 번 더 갈아업어 계산 양을 줄이고 깔끔하게 코드를 만들었으며, 최종본을 설계할때를 기준으로 설명을 한다.

     

    먼저 버튼과 관련하여 함수를 쓸 button.h와 오실로스코프 화면과 관련된 함수를 쓸 ociloscope.h로 파일을 분리하였다.

     

    버튼0 : 데이터 수집 간격 조절, INT0 인터럽트 FALLING, 풀업

    버튼1:  ADC 체널 변경,  INT1 인터럽트 FALLING, 풀업

    버튼 2: 화면 멈추기,  PCINT2 인터럽트, 풀업

     

    버튼이 눌리면 카운트가 올라가고, 나머지 연산을 통해 버튼 상태가 순환하도록 하였다. 또한 채터링이 완벽하게 해결된 것도 아니고, 실수로라도 너무 빨리 버튼을 여러번 누르지 않게 하기 위해 0.5초 안에서 다시 인터럽트가 발생하더라도 무시하도록 코딩을 하였다. (혹시 이 코드를 다른데 활용한다면 PCINT2 인터럽트는 핀변화감지 인터럽트이라 일정 시간 안에서는 다시 인터럽트가 호출되도 무시하는 코드가 필요하다는 것을 명심하라)

     

    button.h

     

    /*
    Button for Arduino Ociloscope
    
    button0 샘플링 간격 변경
    button1 ADC 포트 변환
    button2 화면 정지
    */
    //Macro
    #ifndef cbi //clear bit
    #define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
    #endif
    
    #ifndef sbi //set bit
    #define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
    #endif
    
    #ifndef BUTTON
    #define BUTTON
    #include "Arduino.h"
    
    /*
    전역 변수를 여러 파일에서 공유해서 사용하는 경우, 헤더에는 extern을 사용하고, cpp 파일에 정의한다.
    헤더 파일은 여기저기서 include 되는데 그때마다 정의되면 multiple defination 오류가 뜬다.
    cpp 에만 정의하면 include할 때마다 그 파일에서 extern을 남발해야함, 헤더에서 extern이 미리 정의됨
    컴파일러는 최종적으로 나누어져있는 정의를 모아서 main 함수 위에 붙여넣기 하는 식으로 작동되는 것을 기억하자
    */
    
    extern volatile uint8_t button0LV; //샘플링 간격 변경
    extern volatile uint8_t button1LV; //ADC 핀 변경
    extern volatile uint8_t button2LV; //화면 정지
    
    void INIT_INT0(); //button0 설정
    void INIT_INT1(); //button1 설정
    void INIT_PCINT2(); //button2 설정
    #endif

      

    button.cpp

    #include "button.h"
    volatile unsigned long time0 = 0;
    volatile unsigned long time1 = 0;
    volatile unsigned long time2 = 0;
    
    volatile uint8_t button0LV = 0; //샘플링 간격 변경
    volatile uint8_t button1LV = 0; //ADC 핀 변경
    volatile uint8_t button2LV = 0; //화면 정지
    
    //button0 샘플링 간격 변경
    void INIT_INT0() {
      cbi(DDRB,PD2); //2번핀 입력으로
      sbi(EIMSK,INT0); //INT0 인터럽트 활성화
      sbi(EICRA,ISC01); //Falling 에서 활성화
      sei(); //set interupt, 인터럽트 전역적으로 활성화
    };
    
    ISR(INT0_vect) {
      if(millis()-time0 > 500) {
        button0LV++;
        button0LV %= 6; //6단계 제한
        time0 = millis();
      }
    };
    
    //button1 ADC 포트 변환
    void INIT_INT1() {
      cbi(DDRB,PD3); //3번핀 입력으로
      sbi(EIMSK,INT1); //INT1 인터럽트 활성화
      sbi(EICRA,ISC11); //Falling 에서 활성화
      sei(); //set interupt, 인터럽트 전역적으로 활성화
    };
    
    ISR(INT1_vect) {
      if(millis()-time1 > 500) {
        button1LV++;
        button1LV %= 2;
        time1 = millis();
      }
    };
    
    //button2 화면 정지
    void INIT_PCINT2() {
      cbi(DDRB,PD4); //4번핀 입력으로
      sbi(PCICR,PCIE2); //PCINT2 인터럽트 활성화
      sbi(PCMSK2,PCINT20); //4번핀의 PCINT2 인터럽트 활성화
      sei();
    };
    
    ISR(PCINT2_vect) { //메인에서 button2LV이 1인지 감시해서 while 루프 진입
      if(millis()-time2 > 500) {
        button2LV++;
        button2LV %= 2;
        time2 = millis();
      }
    };

    이렇게 코딩한 하면 버튼0번인 경우 카운트가 0,1,2,3,4,5가 되었다 한번 더 누르면 6되고 6으로 나눈 나머지가 0이 되므로 다시 0으로 카운트가 되돌아가게 된다. 눌린 횟수마다 샘플링 간격을 5ms 증가시킨다.

    버튼1번은 메인파일에서 button1LV을 이용해 switch 문을 사용할 것이며, 버튼2번은 메인파일에서 while(button2LV);라고만 적어주면 해결된다.

     

    그럼 이제 메인이 되는 클래스를 설계하자

    #ifndef OCILOSCOPE
    #define OCILOSCOPE
    
    #include "Arduino.h"
    #include <SPI.h>
    #include <Wire.h>
    #include <Adafruit_GFX.h>
    #include <Adafruit_SSD1306.h>
    
    /*
    define 매크로의 문제점: 고대로 치환된 후 컴파일되다는 점에 유의하기, 괄호 필수
    
    예시
    #define FIVE 5
    #define A FIVE+1
    Serial.println(A*2);
    의도는 A = 6이라 12의 결과값이지만
    컴파일러는
    Serial.println(FIVE+1*2);
    Serial.println(5+1*2);
    Serial.println(7); //최적화: 상수 폴딩
    가 되어 7로 출력된다.
    */
    
    #ifndef SCREEN_WIDTH
    #define SCREEN_WIDTH 128
    #endif
    
    #ifndef SCREEN_HEIGHT
    #define SCREEN_HEIGHT 64
    #endif
    
    #define TEXT_SIZE 1 //1이면 6*8 2이면 12*16 3이면 18*24 ...
    
    //그래프 시작 커서 좌표
    #define GRAPH_X0 (18*TEXT_SIZE+8) //26 //글자와 그래프 사이 여백 9pixel
    #define GRAPH_Y0 (6*TEXT_SIZE+5) //11 //글자와 그래프 사이 여백 6pixel
    
    //ADC 포트 정보가 표시되는 커서 위치
    #define PIN_X0 0
    #define PIN_Y0 0
    
    #define MAX_X0 PIN_X0
    #define MAX_Y0 GRAPH_Y0
    
    #define MIN_X0 PIN_X0
    #define MIN_Y0 (SCREEN_HEIGHT-1-8*TEXT_SIZE) //55
    
    #define MID_X0 PIN_X0
    #define MID_Y0 (MAX_Y0+MIN_Y0)/2 //33
    
    #define AVG_X0 (SCREEN_WIDTH-1-48*TEXT_SIZE) //79
    #define AVG_Y0 PIN_Y0
    
    #define TERM_X0 ((PIN_X0+24*TEXT_SIZE+AVG_X0)/2-18*TEXT_SIZE) //33
    #define TERM_Y0 PIN_Y0
    
    #define GRAPH_WIDTH (SCREEN_WIDTH-GRAPH_X0) //102
    #define GRAPH_HEIGHT (SCREEN_HEIGHT-GRAPH_Y0) //53
    
    #define AXIS_X_NUM 1
    #define AXIS_Y_NUM 3
    
    #define BUF_LENGTH (GRAPH_WIDTH-2) //100
    
    //Macro
    #ifndef cbi //clear bit
    #define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
    #endif
    
    #ifndef sbi //set bit
    #define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
    #endif
    
    void setADC();
    
    class Ociloscope {
        private:
        Adafruit_SSD1306 *displayPtr;
    
        public:
        Ociloscope(Adafruit_SSD1306 *screenPtr);
        void drawAxis();
        void drawInfo(float max, float mid, float min, uint8_t pin, float term, float averge, uint8_t deci = 2);
        void drawLoading();
        void drawGuide();
    };
    #endif

    좌표와 점선 갯수, 그에따른 버퍼 길이처럼 화면크기가 주어졌을 때, 바꾸지 않을 만한 값들은 define을 이용하면 컴파일러가 컴파일 전에 해당 값으로 치환하여 상수를 적어둔 것처럼 작동되므로 최적화하는데 유용하다. 컴파일러의 상수 폴딩 과정에서 상수만으로 된 계산식은 미리 계산되니 참고하자. (const를 사용하면, 수식을 적어낼 수 없다.)

     

    화면을 다시 나눠서 살펴보면, 그래프의 점선과 기준 테두리를 그려줄 drawAxis

    계산한 정보를 화면에 표시해줄 drawInfo

    로딩화면과 가이드 화면을 띄어줄 drawLoading, drawGuide를 생각해볼 수 있다.

    ADC 변환속도를 빠르게 하기 위해 ADC에 공급되는 클럭 분주비를 변경하는 setADC()함수를 추가하였다.

     

    이렇게 헤더파일에 클래스를 선언하면서 무슨 기능이 필요할지 생각하며 매서드 이름을 정해두면, cpp파일에서 구현은 그리 어렵지 않다. 

     

    이들은 모두 SSD1306에서 제공하는 매서드를 사용해야 하는데, 메인파일에서처럼 oled.clear()이리 사용할 수 없다. 왜냐하면 지금 선언한 내용은 #include로 불러오니 oled라도 하는 객체가 정의되기 정의되기 전에 전처리 되기 때문이다. 

    상속을 받거나 포인터 주소를 전달받는 방법이 있는데, 그래픽 관련처럼 덩치큰 인스턴스는 포인터 주소로 전달받는게 최적화에 도움이 된다. SSD1306이 Ociloscope 뿐만이 아니라 A,B,C 다른 클래스에서도 상속된다고 하면, 그래픽 인스턴스가 A,B,C 안에서도 생성자가 호출되어 각각 별도의 메모리를 차지하는데, 비효율적이지 않은가? 포인터 주소로 넘겨받으면 실질적으로 그래픽 인스턴스 한 개 분량의 메모리만 차지하고, 그 기능을 주소를 넘겨받은 클래스에서 다 쓸 수 있다.

     

    #include "Ociloscope.h"
    
    void setADC() {
        //ADC 클럭 1MHz, 변환에 13ADC클럭이 소요된다고 알려져있으니 변환속도가 1MHz/13≒77KHz
        sbi(ADCSRA, ADPS2);
        cbi(ADCSRA, ADPS1);
        cbi(ADCSRA, ADPS0);
    };
    
    Ociloscope::Ociloscope(Adafruit_SSD1306 *screenPtr) {
        displayPtr = screenPtr;
    }
    
    void Ociloscope::drawAxis()
    {
        //왼쪽 기준축
        displayPtr->drawLine(GRAPH_X0, GRAPH_Y0, GRAPH_X0, SCREEN_HEIGHT-1, SSD1306_WHITE);
        displayPtr->drawLine(GRAPH_X0, GRAPH_Y0, GRAPH_X0+3, GRAPH_Y0, SSD1306_WHITE);
        displayPtr->drawLine(GRAPH_X0, SCREEN_HEIGHT-1, GRAPH_X0+3, SCREEN_HEIGHT-1, SSD1306_WHITE);
    
        //오른쪽 기준축
        displayPtr->drawLine(SCREEN_WIDTH-1, GRAPH_Y0, SCREEN_WIDTH-1, SCREEN_HEIGHT-1, SSD1306_WHITE);
        displayPtr->drawLine(SCREEN_WIDTH-1, GRAPH_Y0, SCREEN_WIDTH-4, GRAPH_Y0, SSD1306_WHITE);
        displayPtr->drawLine(SCREEN_WIDTH-1, SCREEN_HEIGHT-1, SCREEN_WIDTH-4, SCREEN_HEIGHT-1, SSD1306_WHITE);
    
        //가로 점선
        for(int n=0; n<AXIS_X_NUM; n++) {
            for(int i=GRAPH_X0; i<SCREEN_WIDTH-1; i+=7) displayPtr->drawLine(i, GRAPH_Y0+GRAPH_HEIGHT*(n+1.0)/(AXIS_X_NUM+1.0), i+2, GRAPH_Y0+GRAPH_HEIGHT*(n+1.0)/(AXIS_X_NUM+1.0), SSD1306_WHITE);
        }
    
        //세로 점선
        for(int n=0; n<AXIS_Y_NUM; n++) {
            for(int i=GRAPH_Y0; i<SCREEN_HEIGHT-1; i+=7) displayPtr->drawLine(GRAPH_X0+GRAPH_WIDTH*(n+1)/(AXIS_Y_NUM+1), i, GRAPH_X0+GRAPH_WIDTH*(n+1)/(AXIS_Y_NUM+1), i+2, SSD1306_WHITE);
            displayPtr->drawLine(GRAPH_X0+GRAPH_WIDTH*(n+1)/(AXIS_Y_NUM+1)-1, GRAPH_Y0, GRAPH_X0+GRAPH_WIDTH*(n+1)/(AXIS_Y_NUM+1)+1, GRAPH_Y0, SSD1306_WHITE); //상단 장식
            displayPtr->drawLine(GRAPH_X0+GRAPH_WIDTH*(n+1)/(AXIS_Y_NUM+1)-1, SCREEN_HEIGHT-1, GRAPH_X0+GRAPH_WIDTH*(n+1)/(AXIS_Y_NUM+1)+1, SCREEN_HEIGHT-1, SSD1306_WHITE); //하단 장식
            displayPtr->drawLine(GRAPH_X0+GRAPH_WIDTH*(n+1)/(AXIS_Y_NUM+1), SCREEN_HEIGHT-1, GRAPH_X0+GRAPH_WIDTH*(n+1)/(AXIS_Y_NUM+1), SCREEN_HEIGHT-3, SSD1306_WHITE); //끝부분에 라인이 안 그려졌을 때 허전하지 않기 위한 하단 장식2
        }
    };
    
    void Ociloscope::drawInfo(float max, float mid, float min, uint8_t pin, float term, float averge, uint8_t deci = 2) {
        displayPtr->setTextColor(WHITE);
        displayPtr->setTextSize(TEXT_SIZE);
    
        displayPtr->setCursor(MAX_X0, MAX_Y0);
        displayPtr->print(max, deci);
    
        displayPtr->setCursor(MID_X0,MID_Y0);
        displayPtr->print(mid, deci);
    
        displayPtr->setCursor(MIN_X0,MIN_Y0);
        displayPtr->print(min, deci);
    
        displayPtr->setCursor(PIN_X0,PIN_Y0);
        displayPtr->print("ADC" + String(pin));
    
        displayPtr->setCursor(TERM_X0,TERM_Y0);
        displayPtr->print(String(term) + "ms"); 
    
        displayPtr->setCursor(AVG_X0,AVG_Y0);
        displayPtr->print("avg" + String(averge) + "V");
    };
    
    void Ociloscope::drawLoading() {
        displayPtr->clearDisplay();
        displayPtr->setTextColor(WHITE);
        displayPtr->setTextSize(3); //글자 18*24
        displayPtr->setCursor(0,0);
        displayPtr->print("Arduino");
    
        displayPtr->setCursor(0,30);
        displayPtr->setTextSize(2);
        displayPtr->print("Ociloscope");
    };
    
    void Ociloscope::drawGuide(){
        displayPtr->setTextColor(WHITE);
        displayPtr->setTextSize(TEXT_SIZE);
    
        displayPtr->setCursor(MAX_X0,MAX_Y0);
        displayPtr->print("Max");
    
        displayPtr->setCursor(MID_X0,MID_Y0);
        displayPtr->print("Mid");
    
        displayPtr->setCursor(MIN_X0,MIN_Y0);
        displayPtr->print("Min");
    
        displayPtr->setCursor(PIN_X0,PIN_Y0);
        displayPtr->print("Pin");
    
        displayPtr->setCursor(TERM_X0,TERM_Y0);
        displayPtr->print("Term"); 
    
        displayPtr->setCursor(AVG_X0,AVG_Y0);
        displayPtr->print("Averge");
    
        this->drawAxis();
    };

     

    마지막으로 메인 파일이다. 

    /*
    Arduino Ociloscope
    Begin 2023-01-18
    version: 3.0
    
    최소 화면 크키 128*64
    
    핀 연결
    OLED
    SCL 13
    SDA 11
    RES 8
    DC 9
    (CS핀이 없는 모듈은 1:n 통신 불가)
    
    버튼 2번핀:데이터 수집 시간 간격 변경
    INT0 인터럽트 사용, 풀업 저항
    
    버튼 3번핀: ADC 변경
    INT1 인터럽트 사용, 풀업 저항
    
    버튼 4번핀: 화면 정지
    PCINT2 인터럽트 사용, 풀업 저항
    */
    
    //OLED
    #include <SPI.h>
    #include <Wire.h>
    #include <Adafruit_GFX.h>
    #include <Adafruit_SSD1306.h>
    
    #define SCREEN_WIDTH 128
    #define SCREEN_HEIGHT 64
    
    #define OLED_MOSI 11 //SDA라 표기됨
    #define OLED_CLK 13 //SCL라 표기됨
    #define OLED_DC 9
    #define OLED_CS 10
    #define OLED_RESET 8
    Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT, OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);
    
    #include "src/button.h"
    
    #include "src/Ociloscope.h"
    Ociloscope scope(&oled);
    
    unsigned int buffer[BUF_LENGTH];
    
    #define A1_R1 30.0
    #define A1_R2 7.5 
    
    //이유는 정확히 모르겟지만 inline 함수를 파일을 분리해서 오버로딩하면 undefined reference 오류뜸
    inline float  cVol(uint16_t x) {
        return x*(5.0/1023.0);
    };
    
    inline float  cVol(uint16_t x, float r1, float r2) {
        return x*(5.0/1023.0)*((r1+r2)/r2);
    };
    
    void setup() {
        INIT_INT0();
        INIT_INT1();
        INIT_PCINT2();
        setADC();
        oled.begin(SSD1306_SWITCHCAPVCC);
    
        oled.clearDisplay();
        scope.drawLoading();
        oled.display();
        delay(2500);
    
        oled.clearDisplay();
        scope.drawGuide();
        oled.display();
        delay(2500);
    }
    
    void loop() {
        uint8_t pin = A0 + button1LV;
        unsigned long Sum = 0;
        float Max = 0.0, Min = 0.0, Mid = 0.0, Term = 0.0, Averge = 0.0;
    
        //데이터 수집
        if(!button0LV) {
            Term = BUF_LENGTH/77.0;
            for(int i=0; i<BUF_LENGTH; i++) {
                buffer[i] = analogRead(pin);
            }
        }
        else {
            for(int i = button0LV; i<6; i++) if(5*i- BUF_LENGTH/77.0 > 0) {Term = 5*i; button0LV = i; break;} //Term이 양수가 되도록 조정
            Term = 5*button0LV;
            unsigned int delayTerm = (Term - BUF_LENGTH/77.0)/BUF_LENGTH*1000.0;
            for(int i=0; i<BUF_LENGTH; i++) {
                buffer[i] = analogRead(pin);
                delayMicroseconds(delayTerm);
            }
        }
    
        //데이터 처리&그래프 그리기
        oled.clearDisplay();
        uint16_t pri = map(buffer[0],0,1023,SCREEN_HEIGHT-1,GRAPH_Y0);
        for(int i=0; i<BUF_LENGTH; i++) {
             Max = max(Max, buffer[i]);
             Min = min(Min, buffer[i]);
             Sum += buffer[i];
    
            uint16_t temp = map(buffer[i],0,1023,SCREEN_HEIGHT-1,GRAPH_Y0);
            oled.drawPixel(GRAPH_X0+1+i, temp, SSD1306_WHITE);
            oled.drawLine(GRAPH_X0+1+i, pri, GRAPH_X0+1+i, temp, SSD1306_WHITE);
            pri = temp;
        }
    
        switch(pin) {
            case A0:
            Max = cVol(Max);
            Min = cVol(Min);
            Mid = (Max + Min)/2.0;
            Averge = cVol(Sum/BUF_LENGTH);
            scope.drawInfo(Max,Mid,Min,button1LV,Term,Averge);
            break;
    
            case A1:
            Max = cVol(Max,A1_R1,A1_R2);
            Min = cVol(Min,A1_R1,A1_R2);
            Mid = (Max + Min)/2.0;
            Averge = cVol(Sum/BUF_LENGTH,A1_R1,A1_R2);
            scope.drawInfo(Max,Mid,Min,button1LV,Term,Averge,1);
            break;
        }
        scope.drawAxis();
        oled.display();
    
        while(button2LV);
    }

    참고로 루프 전체의 실행시간이 얼마나 걸릴지 컴파일 단계에서 알기 어렵다. 그러나 데이터를 수집하는 순간에는 ADC에 공급되는 클럭과 변환에 13ADC클럭 정도 걸린다는 정보를 바탕으로 비교적 정확하게 샘플링 시간을 구할 수 있으며, 조절 역시 가능하다. 이리하면 한 화면 안에서는 비교적 정확하게 파형을 보여줄 수 있게 된다.

     

    https://github.com/sidreco214/Ociloscope

     

    GitHub - sidreco214/Ociloscope: Arduino Ociloscope

    Arduino Ociloscope. Contribute to sidreco214/Ociloscope development by creating an account on GitHub.

    github.com

     

     

    번외

    화면에 여유 공간이 없어 넣지는 않았지만 진동수 구하는 알고리즘을 생각해보면, 버퍼의 0번째 값과 가장 가까운 버퍼의 순서를 찾는 것이다. 테스트 해보진 않았지만 데이터 처리 부분을 이렇게 바꿔주면 될 것 같다.

    #define FREQ_X (원하는 좌표)
    #define FREQ_Y (원하는 좌표)
    
    //데이터 처리&그래프 그리기
        oled.clearDisplay();
        uint16_t pri = map(buffer[0],0,1023,SCREEN_HEIGHT-1,GRAPH_Y0);
        uint8_t near_num = 0;
        uint16_t val = 1023;
        for(int i=0; i<BUF_LENGTH; i++) {
             Max = max(Max, buffer[i]);
             Min = min(Min, buffer[i]);
             Sum += buffer[i];
             
             if(abs(buffer[i]-buffer[0]) < val) {
                val = abs(buffer[i]-buffer[0]);
                near_num = i;
             }
    
            uint16_t temp = map(buffer[i],0,1023,SCREEN_HEIGHT-1,GRAPH_Y0);
            oled.drawPixel(GRAPH_X0+1+i, temp, SSD1306_WHITE);
            oled.drawLine(GRAPH_X0+1+i, pri, GRAPH_X0+1+i, temp, SSD1306_WHITE);
            pri = temp;
        }
        
        if(!button0LV) float cycle = near_num/77.0; //단위 ms
        else           float cycle = near_num*delayTerm;
        float freq = 1.0/cycle; //단위 KHz
        
        if(freq < 1.0) {
        	freq *= 1000;
            setCusor(FREQ_X,FREQ_Y);
            oled.print(String(freq) + "Hz");
        }
        else if(999.9 < freq) {
        	freq /= 1000;
            setCusor(FREQ_X,FREQ_Y);
            oled.print(String(freq) + "MHz");
        }
        else {
            setCusor(FREQ_X,FREQ_Y);
            oled.print(String(freq) + "KHz");
        };

     

    댓글

Designed by Tistory.