ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 아두이노 로터리 인코더로 UI 컨트롤 하기
    아두이노 2023. 2. 5. 14:30

    출처 https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=ymlove78&logNo=221939511527
    출처 https://ko.aliexpress.com/item/1005004330005903.html?pdp_npi=2%40dis%21KRW%21%E2%82%A9%2016%2C091%21%E2%82%A9%209%2C649%21%21%21%21%21%402101d1c316754949210421664e0172%2112000028779983763%21btf&_t=pvid%3A0bfa9b53-06f5-4094-9f7b-70c442090857&afTraceInfo=1005004330005903__pc__pcBridgePPC__xxxxxx__1675494921&spm=a2g0o.ppclist.product.mainProduct&gatewayAdapt=glo2kor

    사진처럼 노브를 돌려 커서를 움직이고, 눌러서 선택하는 이런 인터페이스는 여러 전자제품에서 볼 수 있다. 버튼을 여러개 만드는 것보다 로터리 인코더를 사용하는 것이 포트를 적게 사용할 수 있기 때문이다. 다른 프로젝트를 할 때 많이 사용할 수 있을 것 같아 만들어 보았다.

     

    로터리 인코더 

    참고 https://elecs.tistory.com/181

     

    [로터리 엔코더] 엔코더의 작동 원리 및 사용 방법

    모종의 일로 엔코더를 접하게 된 기회가 생겨 관련 내용을 조사해 보았는데 엔코더를 완전히 처음 접하게 되는 저의 입장에서 인터넷 상에 올라온 엔코더에 대한 정보를 이해하는데 상당히 많

    elecs.tistory.com

    출처https://m.blog.naver.com/emperonics/222108739792

    A와, B는 Vcc에 연결되어있고, C는 그라운드에 연결되어있다. 만약 시계방향으로 돈다면 A가 먼저 디스크에 닿아 0V로 떨어지고, 다음 번 구멍을 만날때 먼저 5V로 상승하게 될 것이다. 

    시계방향

     

    출처 https://elecs.tistory.com/181

    반시계 방향

    출처 https://elecs.tistory.com/181

    그림처럼 A상과 B상은 주기가 같으며, 위상이 90도 벌어져있다. 로터리 인코더를 프로그램 상에서 계속 감시하기에는 LCD를 표시하는 과정에서 딜레이를 걸어야하기 때문에 신호를 놓일 공산이 크다.(LCD clear와 write가 너무 빠르게 반복되면 내용이 제대로 안보인다) 전체 코드가 짧다면, 딜레이 대신

    int16_t count;
    
    ...
    
    unsigned long time0 = millis();
    while(millis()-time0 < 500) {
    	if(!digitalRead(2)) {
    		uint8_t state = digitalRead(3);
    		if(state) count--;
    		else      count++;
        }
    }

    이렇게 처리할 수 있겠지만, 처리해야될 버튼 입력이 늘어냐면 if문이 점점 늘어나고, 코드도 지저분해진다. 게다가 로터리 인코더로 각속도를 측정하는 경우, 정확성이 떨어지는 원인이된다. 

     

    아두이노에서는 INTn나 PCINTn 인터럽트를 사용하면 이러한 문제가 깔끔하게 해결된다.(PCINTn 인경우 인터럽트 호출당시 RISING인지 FALLING인지 판단하는 과정이 필요하다)

    A상을 기준으로 잡고, INT0 인터럽트를 RISING으로 설정하였다면, 인터럽트를 호출하였을 때, B가 LOW면 시계, HIGH면 반시계 방향으로 회전한 것이다. 인터럽트에서 시계 뱡향이라면 count를 ++하고 반시계 방향이면 --하면 얼마나 회전했는지까지 알 수 있다.

     

    로터리 인코더를 살 때 스펙에 step이라고 나와있는 부분이 있는데, 이는 360도를 회전하였을 때 펄스가 몇번 발생하는지를 나타낸 것이다. 즉 step이 20이면 RISING INT0 인터럽트 20번 호출된다. 이를 이용하여 회전한 각도를 구할 수 있으며, 이전인터럽트 발생과 현 인터럽트 발생 사이 시간 간격을 millis()로 측정하여 각속도를 계산할 수도 있다. 각도와 각속도 계산은 혹시 나중에 쓸 수 있지 않을까 하여 구현은 해놨지만 테스트하지는 않았다.

     

    RotaryEN.h

    /*
    Rotary Encoder
    로터리 인코더의 회전 방향과 스탭, 회전 각도, 각속도를 계산할 수 있음
    참고 https://elecs.tistory.com/181
    참고하기 전에 내가 설계했을 때는 인터럽트 INT0, INT1 두개 썻는데, 이상하게 작동할 때 있음
    */
    
    //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 ROTARY
    #define ROTARY
    #include <avr/io.h>
    #include <avr/interrupt.h>
    #include "Arduino.h"
    
    void INIT_INT0();   //s1핀의 RISING
    
    //버튼 입력
    void INIT_PCINT(); //D포트 0~7, B포트 8~13, C포트 14~19
    void END_PCINT();
    inline void button(); //버튼 입력시 인터럽트 처리 함수
    
    class RotaryEN {
        protected:
        uint8_t _step; //로터리 인코더 스탭
    
        public:
        RotaryEN(); //방향만 필요한 경우
        RotaryEN(uint8_t button); //방향+버튼 입력
        RotaryEN(uint8_t button, uint8_t step); //몇도 회전했는지 까지 필요한 경우
        ~RotaryEN();
    
        uint8_t pressed(); //버튼이 눌러졌으면 1
        int16_t step(); //로터리 인코더 회전 스탭 반환
        inline float rottation(uint8_t step); //로터리 인코더 회전 각도 변환(단위: 도)
        inline float velocity(uint8_t step);
    
    };
    #endif

    RotrayEN.cpp

    #include "RotaryEN.h"
    
    /*
    인터럽트 내에서 방향을 파악한 뒤, count++ or count--
    s1 A상
    s2 B상
    */
    volatile int16_t count = 0; //s1 핀의 RISING 카운트
    volatile unsigned long pri; //각속도 측정용 이전 시간 저장
    volatile unsigned long time; //걸린 시간
    
    void INIT_INT0() {
      cbi(DDRB,PD2); //2번핀 입력으로
      cbi(DDRB,PD3); //3번핀 입력으로
      sbi(EIMSK,INT0); //INT0 인터럽트 활성화
      EICRA |= 0x03; //RISING 에서 활성화
      sei(); //set interupt, 인터럽트 전역적으로 활성화
    };
    
    ISR(INT0_vect) {
        int8_t state = digitalRead(3);
        if(state) count--;
        else      count++;
        time = millis() - pri;
        pri = millis();
    };
    
    volatile uint8_t buttonLV = 0;
    volatile unsigned long t = 0;
    uint8_t pin;
    
    void INIT_PCINT() {
        if(pin < 8) {
            //D포트(0~7) PCINT16~23
            cbi(DDRD,pin);
            sbi(PCICR,PCIE2); //PCINT2 인터럽트 활성화
            sbi(PCMSK2,pin); //해당 핀의 PCINT2 인터럽트 활성화
        }
        else if(pin < 14) {
            //B포트(8~13) PCINT0~7
            cbi(DDRB,(pin-8));
            sbi(PCICR,PCIE0); //PCINT0 인터럽트 활성화
            sbi(PCMSK0,(pin-8)); //해당 핀의 PCINT0 인터럽트 활성화
        }
        else if(pin < 20) {
            //C포트(14~19) PCINT14~19
            cbi(DDRC,(pin-14));
            sbi(PCICR,PCIE1); //PCINT1 인터럽트 활성화
            sbi(PCMSK1,(pin-14)); //해당 핀의 PCINT1 인터럽트 활성화
        }
        else return;
        sei();
    };
    
    void END_PCINT() {
        if(pin < 8) {
            //D포트(0~7) PCINT16~23
            cbi(PCICR,PCIE2); //PCINT2 인터럽트 비활성화
            cbi(PCMSK2,pin); //해당 핀의 PCINT2 인터럽트 비활성화
        }
        else if(pin < 14) {
            //B포트(8~13) PCINT0~7
            cbi(PCICR,PCIE0); //PCINT0 인터럽트 비활성화
            cbi(PCMSK0,(pin-8)); //해당 핀의 PCINT0 인터럽트 비활성화
        }
        else if(pin < 20) {
            //C포트(14~19) PCINT14~19
            cbi(PCICR,PCIE1); //PCINT1 인터럽트 활성화
            cbi(PCMSK1,(pin-14)); //해당 핀의 PCINT1 인터럽트 비활성화
        }
        else return;
    }
    
    inline void button() {
        if(millis()-t > 500 && digitalRead(pin)) {
            buttonLV = 1;
        }
    };
    
    ISR(PCINT0_vect) {
      button();
    };
    
    ISR(PCINT1_vect) {
      button();
    };
    
    ISR(PCINT2_vect) {
      button();
    };
    
    RotaryEN::RotaryEN() {
        pri = millis();
        INIT_INT0();
    }
    
    RotaryEN::RotaryEN(uint8_t button) {
        pri = millis();
        pin = button;
        INIT_INT0();
        INIT_PCINT();
    }
    
    RotaryEN::RotaryEN(uint8_t button, uint8_t step) {
        pri = millis();
        pin = button;
        INIT_INT0();
        INIT_PCINT();
       
        _step = step;
    }
    
    RotaryEN::~RotaryEN() {
        cbi(EIMSK,INT0); //INT0 인터럽트 비활성화
        END_PCINT();
        count = 0;
        pri = 0;
        time = 0;
        buttonLV = 0;
        t = 0;
        pin = 0;
    }
    
    uint8_t RotaryEN::pressed() {
        if(buttonLV) {buttonLV = 0; return 1;}
        else         return 0;
    }
    
    int16_t RotaryEN::step() {
        int16_t temp = count;
        count = 0;
        return temp;
    }
    
    inline float RotaryEN::rottation(uint8_t pulse) {
        return pulse*(360.0/_step);
    }
    
    inline float RotaryEN::velocity(uint8_t pulse) {
        return this->rottation(pulse)/time;
    }

    회로 연결

     로터리 인코더 스위치형

    로터리 인코더 아두이노
    GND GND
    s1(B상) 3
    s2(A상) 2
    key 4
    5V 5V

    A상 B상이 아니라 위상차를 출력하는 모델도 있으니 주의

    무슨핀이 A상이고 B상인지 모른데, 데이터시트 찾아보기 귀찮으면, 아무거나 해보고, 거꾸로 작동하면 핀을 바꿔주면 된다.

     

    LCD i2c 2004

    LCD i2c 2004 아두이노
    GND GND
    VCC 5V
    SDA SDA(A4)
    SCL SCL(A5)

    13번 핀 옆에 SDA, SCL 핀이 있으며, 아두이노를 뒤집어 보면 무슨핀인지 적혀있다. 

     

    코딩

    https://github.com/sidreco214/RotaryEN

     

    GitHub - sidreco214/RotaryEN: Rotary Encoder Interface with LCD

    Rotary Encoder Interface with LCD. Contribute to sidreco214/RotaryEN development by creating an account on GitHub.

    github.com

    자료구조 : 트리

    출처 https://gmlwjd9405.github.io/2018/08/12/data-structure-tree.html

    설계할 UI는 여러 선택지 중 하나를 선택하면, 다시 하위에 여러 선택지가 나오는 UI이며, 트리형으로 만드는게 새로 선택지를 추가하거나 지우는 등의 유지보수가 편하다. 노드가 담을 정보로는 UI에 표시될 이름, 이전노드 주소, 자식노드 주소, 자식노드 수, switch로 실행할 command 넘버이다.

    //트리 자료형
    class Node {
        public:
        String name;
        Node* pri; //이전 노드
        Node** child; //하위 자식노드 배열 저장
        uint8_t length; //하위 자식노드 갯수
        uint8_t command;
    
        Node(String Name = "", uint8_t childSize = 0, Node* Pri = NULL, uint8_t Command = 0) {
            name = Name;
            length = childSize;
            child = NULL;
            command = Command;
            pri = Pri;
        };
    
        ~Node() {
          if(child) {
            delete [] child;
            child = NULL;
          }
        }
    };
    
    #define BACK 1
    enum Order {
        menu_A1 = 2,
        menu_A2,
    
        menu_B1,
    
        menu_C1,
    
        menu_D1,
    
        menu_E1,
    } order;
    
    ...
    
    //UI 내용 할당
    Node root("",5);
    Node menuA("A",3,&root);
    Node menuB("B",2,&root);
    Node menuC("C",2,&root);
    Node menuD("D",2,&root);
    Node menuE("E",2,&root);
    root.child =  new Node*[5]{&menuA, &menuB, &menuC, &menuD, &menuE};
    
    Node menuA1("A1",0,&menuA,menu_A1);
    Node menuA2("A2",0,&menuA,menu_A2);
    Node menuA3("Back",0,&menuA,BACK);
    menuA.child = new Node*[3]{&menuA1, &menuA2, &menuA3};
    
    ...

    참고로 자식 노드 포인터 배열 할당할 때 &(엠퍼센트) 쓰기 귀찮다면,

    1.전역으로 사용하는 경우

    Node* root = new Node("",5);
    ...
    
    void setup() {
     root->child = new Node*[5]{menuA,menuB,menuC,menuD,menuE};
    }

    이렇게 선언해주면 된다. (다만 이방식은 Node 선언과 child 선언이 멀리 떨어져서 관리하기 귀찮다. 메인에다 사용하면 묶어서 쓸 수 있음) 만약 메모리를 아끼고자 loop 안에 쓴다면 delete를 제대로 해주어야 메모리 누수가 발생하지 않는다.

     

    2.스마트 포인터

    참고 https://min-zero.tistory.com/entry/C-STL-1-3-%ED%85%9C%ED%94%8C%EB%A6%BF-%EC%8A%A4%EB%A7%88%ED%8A%B8-%ED%8F%AC%EC%9D%B8%ED%84%B0smart-pointer

    아두이노 내부에 memory.h가 없어서, c++ 컴파일러를 다운로드한 뒤 헤더를 찾아서 추가해주거나, 아래처럼 스마트포인터를 간단하게 구현한 후 선언해야 한다.

    class Node {
        public:
        String name;
        Node* pri; //이전 노드
        Node** child; //하위 자식노드 배열 저장
        uint8_t length; //하위 자식노드 갯수
        uint8_t command;
    
        Node(String Name = "", uint8_t childSize = 0, Node* Pri = NULL, uint8_t Command = 0) {
            name = Name;
            length = childSize;
            child = NULL;
            command = Command;
            pri = Pri;
        };
    
        ~Node() {
          if(child) {
            delete [] child;
            child = NULL;
          }
        }
    };
    
    template <class T>
    class SmartPtr {
      private:
      T* ptr;
    
      public:
      SmartPtr(T* p) :ptr(p) {}
      ~SmartPtr() {
        delete ptr;
      }
    
      T* address() const {
        return ptr;
      }
    
      T* operator->() const {
        return ptr;
      }
    };
    
    void setup() {};
    
    void loop() {
        while(1)
            SmartPtr<Node> root(new Node("",2));
            SmartPtr<Node> menuA(new Node("A",0,root.address(),1));
            SmartPtr<Node> menuB(new Node("B",0,root.address(),2));
            root->child = new Node*[2]{menuA.address(),menuB.address()};
            
            ...
            
            while(1) {
                ...
            }
            
            ...
    };

    참고로 ptr(p)는 ptr = p와 같다. c++에 추가된 변수 초기화 방법이다. 스마트 포인터는 클래스를 템플릿으로 일반화시킨것인데, 일반포인터와 다른 점은

    1.수명이 끝나면 자동으로 메모리 할당을 해제해서 메모리 누수를 방지해준다.

    2.포인터 주소가 private 이기때문에 함부로 바뀌지 않아 할당된 메모리 주소를 잃는 일이 없다.

    3.null 포인터 역참조 등, 실수로 인한 각종 사고를 미연에 예방해준다.

    정도 된다.

     

    *추가 2023.02.09

    참고로 이 예처럼 스마트 포인터의 주소를 그냥 다른데다 전달하는 것은 위험하다. 이 경우에는 스마트 포인터의 수명과 노드의 수명이 같아서 문제가 발생하지는 않지만, 스마트 포인터의 수명이 먼저 끝났다면, 노드에서 참조를 할 때, 널포인터를 역참조하는 사고가 발생하게 된다. 이경우 아두이노가 멈추거나, LCD 화면에 이상한 것들이 보일 가능성이 높다. 원래 STL 라이브러리에는 이러한 문제가 있기 때문에 여러 포인터가 한 대상을 참조하는 경우, 레퍼런스 카운트가 0이될 때 할당을 해제하는 shared_ptr이 있다.

    스마트 포인터 구현 https://sidreco.tistory.com/5

     

    아두이노 스마트 포인터 구현

    스마트 포인터가 아두이노에 있으면 편리할 것 같아 구현해보았다. 아두이노는 일반적인 컴퓨터에 비해 렘과 저장공간이 부족하므로 메인기능과 약간에 플러스 알파만 구현하였다. unique_ptr: 하

    sidreco.tistory.com

    화면표시 알고리즘

    cursor = 0
    >A 0123          0123 
     B
     C
     D

    cursor =1
     A                0123
    >B 1230
     C
     D

    cursor =2
     A                0123
     B
    >C 2301
     D

    cursor =3
     A                0123
     B
     C
    >D 3012

    cursor =4
     B                1234
     C
     D
    >E 4123

    cursor =5 나머지 연산 %len

    >A 0123
     B
     C
     D

     cursor 4->3이면
     B                1234
     C
    >D
     E

    이 부분에 대해서 고민을 많이 하였는데 결과적으로 LCD커서와 자료 커서를 만든 뒤, LCD 커서는 constrain으로, LCD 양끝부분에 도달하면 더이상 안내려가거나 안올라가게 하였고, 자료 커서는 나머지 연산으로 순환하도록 만들었다. 그리고 두 커서를 기준으로 위아래로 한칸씩 움직이며 커서 근처에 자료를 참조하여 표시하였다. 

    ...
    while(1) {
        uint8_t len = node->length;
        int16_t count = rotary.step();
                
        uint8_t num = min(LCD_ROW,len);
        LCDcursor += count;
        LCDcursor = constrain(LCDcursor,0, num-1);
    
        cursor += count;
        if(cursor < 0) cursor = 0;
        else           cursor %= len;
        if(LCDcursor > cursor) LCDcursor = cursor;
        //자료 커서가 마지막 E에 있다가 다음으로 넘어가면 자료 커서는 0번을 가르키는데 LCD 커서는 여전히 마지막 줄을 가르키니 초기화 필요
        //그리고 로터리 엔코더를 빨리 돌리면, LCD 커서는 마지막 줄을 가르키는데, 자료 커서는 0,1,2(num 보다 작은 값)을 가르킬 수 있고, 이 경우 문제됨
        //LCD 커서는 항상 자료 커서보다 작거나 같아야 함
    
        lcd.clear();
        //LCD 커서 표시
        lcd.setCursor(0,LCDcursor);
        lcd.print(">");
        
        //위로 진행해서 화면 채우기
        for(int i=0; i<LCD_ROW-LCDcursor; i++) {
            if(LCDcursor+i < LCD_ROW && cursor+i < len) {
                lcd.setCursor(1,LCDcursor+i);
                lcd.print(node->child[cursor+i]->name);
            }
        }
    
        //아래로 진행해서 화면 채우기
            for(int i=1; i<=LCDcursor; i++) {
                if(0 <= LCDcursor-i && 0 <= cursor-i) {
                    lcd.setCursor(1,LCDcursor-i);
                    lcd.print(node->child[cursor-i]->name);
                }
            }    
        ...
      }
      ...

     

    Rotary_Encoder_Interface.ino

    /*
    Rotary Encoder Interface with LCD
    begin 2023-02-01
    
    다른 프로젝트의 일부분이 될 수 있는 로터리 인코더 인터페이스
    메뉴 항목이 많고, 자주 메뉴바가 뜨는 경우 이렇게 코딩하면 메모리 파편화 문제가 있을 수 있음
    이경우 클래스 배열로 선언하거나, 정적으로 선언하는 것을 고려
    
    다만 클래스 배열인 경우, 크게 잡으면 램이 2KB밖에 되지않는 아두이노 우노에서는
    동적할당에 실패하여 버그가 발생할 수 있음, 메뉴바 단위, ex A B C D E 로 할당하면 적당할 듯
    그렇다고 정적으로 선언하자니, 메뉴바가 뜨는 시간은 전체 실행시간에서 얼마 되지 않는데,
    불필요하게 메모리만 차지함
    
    문자열이 크기를 많이 차지하니, 문자열 부분이라도 따로 배열로 만들어 PROGMEM으로 선언해두고,
    노드에는 주소값을 저장한 뒤, 실행할 때 불러오면 어느정도 해결될 듯
    */
    
    //#define DEBUG
    
    #ifndef constrain
    #define constrain(amt,low,high) ((amt)<(low)?(low):((amt)>(high)?(high):(amt)))
    #endif
    
    #include "src/RotaryEN.h"
    
    #include "src/LiquidCrystal_I2C.h"
    #define LCD_ROW 4
    LiquidCrystal_I2C lcd(0x27,20,LCD_ROW);
    
    //트리 자료형
    class Node {
        public:
        String name;
        Node* pri; //이전 노드
        Node** child; //하위 자식노드 배열 저장
        uint8_t length; //하위 자식노드 갯수
        uint8_t command;
    
        Node(String Name = "", uint8_t childSize = 0, Node* Pri = NULL, uint8_t Command = 0) {
            name = Name;
            length = childSize;
            child = NULL;
            command = Command;
            pri = Pri;
        };
    
        ~Node() {
          if(child) {
            delete [] child;
            child = NULL;
          }
        }
    };
    
    #define BACK 1
    enum Order {
        menu_A1 = 2,
        menu_A2,
    
        menu_B1,
    
        menu_C1,
    
        menu_D1,
    
        menu_E1,
    } order;
    
    void setup() {
        #ifdef DEBUG
        Serial.begin(9600);
        #endif
        lcd.init();
        lcd.backlight();
    }
    
    void loop() {
        while(1) {
            RotaryEN rotary(4,20); //로터리 인코더가 메뉴 화면에서만 작동해야 함
    
            //UI 내용 할당
            Node root("",5);
            Node menuA("A",3,&root);
            Node menuB("B",2,&root);
            Node menuC("C",2,&root);
            Node menuD("D",2,&root);
            Node menuE("E",2,&root);
            root.child =  new Node*[5]{&menuA, &menuB, &menuC, &menuD, &menuE};
    
            Node menuA1("A1",0,&menuA,menu_A1);
            Node menuA2("A2",0,&menuA,menu_A2);
            Node menuA3("Back",0,&menuA,BACK);
            menuA.child = new Node*[3]{&menuA1, &menuA2, &menuA3};
    
            Node menuB1("B1",0,&menuB,menu_B1);
            Node menuB2("Back",0,&menuB,BACK);
            menuB.child = new Node*[2]{&menuB1, &menuB2};
    
            Node menuC1("C1",0,&menuC,menu_C1);
            Node menuC2("Back",0,&menuC,BACK);
            menuC.child = new Node*[2]{&menuC1, &menuC2};
            
            Node menuD1("D1",0,&menuD,menu_D1);
            Node menuD2("Back",0,&menuD,BACK);
            menuD.child = new Node*[2]{&menuD1, &menuD2};
    
            Node menuE1("E1",0,&menuD,menu_E1);
            Node menuE2("Back",0,&menuD,BACK);
            menuE.child = new Node*[2]{&menuE1, &menuE2};
    
            Node* node = &root;
            int16_t cursor = 0; //자료 선택커서
            int16_t LCDcursor = 0; //LCD 화면 커서
    
            while(1) {
                uint8_t len = node->length;
                int16_t count = rotary.step();
                
                uint8_t num = min(LCD_ROW,len);
                LCDcursor += count;
                LCDcursor = constrain(LCDcursor,0, num-1);
    
                cursor += count;
                if(cursor < 0) cursor = 0;
                else           cursor %= len;
                if(LCDcursor > cursor) LCDcursor = cursor;
                //자료 커서가 마지막 E에 있다가 다음으로 넘어가면 자료 커서는 0번을 가르키는데 LCD 커서는 여전히 마지막 줄을 가르키니 초기화 필요
                //그리고 로터리 엔코더를 빨리 돌리면, LCD 커서는 마지막 줄을 가르키는데, 자료 커서는 0,1,2(num 보다 작은 값)을 가르킬 수 있고, 이 경우 문제됨
                //LCD 커서는 항상 자료 커서보다 작거나 같아야 함
    
                #ifdef DEBUG
                Serial.print(cursor);
                Serial.print(", ");
                Serial.print(LCDcursor);
                Serial.print(", ");
                Serial.print(count);
                Serial.println("");
                #endif
    
                lcd.clear();
                //LCD 커서 표시
                lcd.setCursor(0,LCDcursor);
                lcd.print(">");
        
                //아래로 진행해서 화면 채우기
                for(int i=0; i<LCD_ROW-LCDcursor; i++) {
                    if(LCDcursor+i < LCD_ROW && cursor+i < len) {
                        lcd.setCursor(1,LCDcursor+i);
                        lcd.print(node->child[cursor+i]->name);
                    }
                }
    
                //위로 진행해서 화면 채우기
                for(int i=1; i<=LCDcursor; i++) {
                    if(0 <= LCDcursor-i && 0 <= cursor-i) {
                        lcd.setCursor(1,LCDcursor-i);
                        lcd.print(node->child[cursor-i]->name);
                    }
                }
        
                if(rotary.pressed()) {
                    if(node->child[cursor]->command) {
                        order = node->child[cursor]->command;
                        cursor = 0;
                        LCDcursor = 0;
                        if(order == BACK) {order = 0; node = node->pri;}
                        else             {break;}
                    }
                    else {
                        node = node->child[cursor];
                        cursor = 0;
                        LCDcursor = 0;
                    }
                }
                else delay(500); //너무 빨리 clear와 쓰기가 반복되면 화면이 제대로 안보임 
            }
            break;
        }
        
        //선택한 메뉴의 기능실행
        switch(order) {
            default:
            break;
    
            case menu_A1:
            #ifdef DEBUG
            Serial.print("func");
            Serial.println(order-1);
            #endif
    
            lcd.clear();
            lcd.setCursor(0,0);
            lcd.print("func");
            lcd.print(order-1);
            order = 0;
            delay(2000);
            break;
    
            case menu_A2:
            #ifdef DEBUG
            Serial.print("func");
            Serial.println(order-1);
            #endif
    
            lcd.clear();
            lcd.setCursor(0,0);
            lcd.print("func");
            lcd.print(order-1);
            order = 0;
            delay(2000);
            break;
    
            case menu_B1:
            #ifdef DEBUG
            Serial.print("func");
            Serial.println(order-1);
            #endif
    
            lcd.clear();
            lcd.setCursor(0,0);
            lcd.print("func");
            lcd.print(order-1);
            order = 0;
            delay(2000);
            break;
    
            case menu_C1:
            #ifdef DEBUG
            Serial.print("func");
            Serial.println(order-1);
            #endif
    
            lcd.clear();
            lcd.setCursor(0,0);
            lcd.print("func");
            lcd.print(order-1);
            order = 0;
            delay(2000);
            break;
    
            case menu_D1:
            #ifdef DEBUG
            Serial.print("func");
            Serial.println(order-1);
            #endif
    
            lcd.clear();
            lcd.setCursor(0,0);
            lcd.print("func");
            lcd.print(order-1);
            order = 0;
            delay(2000);
            break;
    
            case menu_E1:
            #ifdef DEBUG
            Serial.print("func");
            Serial.println(order-1);
            #endif
    
            lcd.clear();
            lcd.setCursor(0,0);
            lcd.print("func");
            lcd.print(order-1);
            order = 0;
            delay(2000);
            break;
        }
    }

    만약 버튼 클릭해서 수치를 조절하는 경우는 노드에 Dcommand를 추가하고, if(rotary.pressed())을 아래처럼 손보면 될 것이다(테스트는 안해봄)

    float data1 = 0.0;
    float data2 = 0.0;
    float data3 = 0.0;
    
    enum Dorder {
    	DO1 = 1,
        DO2,
        DO3,
    } Dorder;
    
    ...
    
    if(rotary.pressed()) {
        if(node->Dcommand) Dorder = node->Dcommand;
        else if(node->child[cursor]->command) {
            order = node->child[cursor]->command;
            cursor = 0;
            LCDcursor = 0;
            if(order == BACK) {order = 0; node = node->pri;}
            else              break;
        }
        else {
        	node = node->child[cursor];
            cursor = 0;
            LCDcursor = 0;
        }
    }
    else delay(500); //너무 빨리 clear와 쓰기가 반복되면 화면이 제대로 안보임
    
    switch(Dorder) {
        default:
        break;
        
        case D01:
        data1 += 0.01*rotary.step();
        Dorder = 0;
        break;
        
        case D02:
        data2 += 0.01*rotary.step();
        Dorder = 0;
        break;
        
        case D03:
        data3 += 0.01*rotary.step();
        Dorder = 0;
        break;
    }

    메모리를 아껴쓴다고 while의 중첩이 많아졌는데, 메모리가 여유롭다면 전역으로 설정해도 무방하다. 사실 main 함수에다 적는다면 조금더 깔끔하게 적을 수 있지만, 아두이노에서 setup과 loop를 안쓰고 main 함수를 써버리면 아두이노에서 제공하는 몇몇 함수가 제대로 작동하지 않는 문제가 있어 조심해야한다.

     

    영상

     

     

    '아두이노' 카테고리의 다른 글

    아두이노 스마트 포인터 구현  (0) 2023.02.10
    아두이노 오실로스코프  (0) 2023.01.31
    아두이노 오르골만들기  (0) 2023.01.30

    댓글

Designed by Tistory.