카운트에 해당하는 글 1

[아두이노] 시간 millis()함수로 시간 카운트

IOT/아두이노|2019. 6. 5. 09:00

[아두이노] 시간 millis()함수로 시간 카운트



지난 시간에 millis()함수를 이용하여 시간값을 각 자리의 숫자들을 하나씩 쪼개는 방법을 실험 했습니다. 이렇게 쪼개진 데이터를 연속적으로 출력하면 먼가 타이머 카운트를 하고 싶어지지 않으신가요. 예를들어, 스위치버턴을 누르면 타이머가 작동하여 카운트를 하다가 다시 스위치를 누르면 그 상태로 정지하고 다시 누르면 처음부터 카운트를 구현해 보고 싶어지더군요. 오늘은 스위치버턴으로 카운트와 타이머시간값에 대해서 실험을 하고자 합니다.

1. 회로도


  • 준비물 : 스위치버턴 1개, 아두이노우노
  • 내용 : 스위치 버턴을 2번핀에 연결하여 내부풀업모드를 이용한다.


실험 회로도는 2번 핀으로 스위치 입력값을 받아서 카운트 동작을 수행합니다. 결과는 시리얼모니터로 출력되기 때문에 별도의 외부 출력부품은 필요 없네요.

2. 코딩



[인터럽트 스위치버턴]

const int interruptPin = 2;//인터럽트핀

void setup() { 
  pinMode(interruptPin, INPUT_PULLUP); 
  attachInterrupt(digitalPinToInterrupt(interruptPin), switchFn, FALLING);
}
  • FALLING : HIGH -> LOW로 변할때

여기서, 왜! FALLING이냐면 내부풀업모드 스위치 버턴은 초기값이 HIGH이고 스위치 버턴을 누르면 LOW가 됩니다. 즉, 스위치 버턴을 누른 순간이 HIGH->LOW로 바뀌는 상태가 되고 인터럽트 함수는 그 상태가 되면 호출이 되어 switchFn()함수를 호출하여 실행시키게 됩니다.


[기존 소스] : 지난시간 타이머 시간값을 쪼개는 소스입니다.

void setup()
{
  Serial.begin(9600);
}
void loop()
{
  unsigned long millisTime = millis(); // 1초 단위면 millisTime = millis()/1000;
  int v1 = millisTime%10;
  int v2 = (millisTime/10)%10;
  int v3 = (millisTime/100)%10;
  int v4 = (millisTime/1000)%10;

  Serial.print(v4);
  Serial.print(" : ");
  Serial.print(v3);
  Serial.print(" : ");
  Serial.print(v2);
  Serial.print(" : ");
  Serial.println(v1);
}

위의 인터럽트 소스랑 지난 시간의 타이머 시간값을 쪼개는 소스를 합쳐서 코딩을 할 예정입니다.

[설계]

  1. 스위치를 누를 때 카운트를 시작하여 결과를 시리얼모니터로 출력한다.
  2. 스위치를 다시 누르면 카운트를 중단하고 시리얼모니터로 카운트 시간값은 더이상 출력되지 않는다.

딱 이 두가지 조건만 수행 합니다.

1) 인터럽트 스위치 이벤트


attachInterrupt(digitalPinToInterrupt(interruptPin), switchFn, FALLING);

위 인터럽트 함수를 선언하면 interruptPin이 HIGH->LOW로 넘어가는 상태가 되면 switchFn()함수가 호출됩니다. 스위치가 누를때 카운트를 하고 다시 스위치를 누르면 카운트가 중단 된다면 이 상황은 어떤 느낌인가요. 스위치를 누르면 두가지 상태가 발생한다는 느낌을 받았다면 정답입니다. 먼지는 모른데 switchFn()함수는 이 두가지 상태를 교대로 발생해야 한다면 두가지 상태를 저장 할 상태변수를 만들면 됩니다.

boolean state=false;

void switchFn(){
  state=!state;
}

위와같이 코딩을 하면 됩니다. 스위치가 눌러지면 switchFn()함수가 호출되고 "state=!state"로 반전값을 갖게 하면 됩니다. false이면 true로 true이면 false로 스위치를 누를 때 마다 state의 값이 true or false로 왔다 갔다 하게 되는 것이죠.

2) 카운트 동작


void loop(){
   if(state==true){ //스위치 상태가 true일때 카운트
      millisTime = millis()/1000;
      d1 = millisTime%10; //1의 자리
      d2 = (millisTime/10)%10;//10의 자리
      d3 = (millisTime/100)%10;//100의 자리
      d4 = (millisTime/1000)%10;//1000의 자리

      Serial.print(d4);
      Serial.print(" : ");
      Serial.print(d3);
      Serial.print(" : ");
      Serial.print(d2);
      Serial.print(" : ");
      Serial.println(d1);        
 }  
}

1초 단위로 각 자리 수들이 d1, d2, d3, d4에 저장됩니다. 그런데, 아두이노는 전원이 공급되면 타이머가 동작하고 그때부터 타이머 숫자가 증가하게 됩니다. 그러면, 스위치가 누른 시점에서 카운트가 시작해야 하는데 어떻게 해야 할까요. 해답은 누른 시간값에서 카운트가 시작할려면 현재 millis()시간에서 카운트를 시작하는 millis() 시간을 빼준 값에서 시작하면 스위치 버턴을 누른 시점부터 카운트를 할 수 있게 됩니다.

  • 현재시간 - 스위치누르시간 = 카운트 시작시간
    millisTime = (millis()-countTime)/1000;

countTime은 스위치가 누른 시간값이라고 하면 이 시간은 어디서 측정 할까요. 바로 스위치가 누른 시점에 countTime 시간을 측정하면 됩다.

void switchFn(){
  state=!state;
  countTime=millis(); //스위치 누른 시간
}

딱 한줄이면 간단히 코딩이 되죠. millisTime 변수에는 스위치가 눌러진 시간에서부터 loop()함수가 계속 반복하면 값이 증가하게 됩니다. 즉, 타이머 카운트를 1초씩 세게 됩니다.

이상태에서 바로 아래와 같이 출력시키면 어떻게 될까요.

      Serial.print(d4);
      Serial.print(" : ");
      Serial.print(d3);
      Serial.print(" : ");
      Serial.print(d2);
      Serial.print(" : ");
      Serial.println(d1);       

[결과]

  • millisTime = (millis()-countTime)/100; 일때

1초단위로 해야하는 시뮬레레이터에서 너무 느려서 0.1초 단위로 숫자를 증가시켰는데 위 결과를 보시는 것처럼 중복된 숫자가 0.1초 동안 연달아서 출력이 됩니다. 그러면 보기 그렇죠.

void switchFn(){
  state=!state;
  countTime=millis();
  timeVal=countTime;
}

스위치가 눌러지면 출력 시간은 이전시간(timeVal)도 스위치가 눌러진 시간으로 지정해 놓으면 그 다음 부터 아래 loop()안에 로직인데 if문을 통해서 1초 단위로 카운트를 세게 됩니다.

void loop(){
 if(state==true){ //카운트시작
    if(millis()-timeVal>=1000){ //1초단위로 세기
      timeVal=millis();
            카운트 동작;
    }
  }  
}

이렇게 조건을 하나 더 만들어 주면 1초 단위로 카운트를 세게 됩니다. 참고로, 이것은 시리얼모니터로 1초 단위로 출력하기 위한 로직입니다. 만약에 7-Segment Display 로 출력한다면 위의 1초 단위로 출력 할 필요가 없습니다. 중복된 숫자가 연속으로 나와도 상관없습니다. 4-Digit 7-Segment Display 경우는 계속 숫자가 출력을 시켜야하는데 중간에 멈추면 숫자가 출력되지 않기 때문에 오히려 문제가 생깁니다. 이건 그냥 시리얼모니터로 출력시키기 위한 조건임을 감안하시고 코딩을 살펴 봐주세요.

3) 종합소스


const int interruptPin = 2;//인터럽트핀
unsigned long timeVal = 0; //이전시간
unsigned long millisTime = 0; //현재시간
unsigned long countTime = 0; //카운트시작시간
int d1, d2, d3, d4;//자리 숫자
boolean state = false;//타이머 동작 제어 

void setup()
{
  Serial.begin(9600);
  pinMode(interruptPin, INPUT_PULLUP); 
  attachInterrupt(digitalPinToInterrupt(interruptPin), switchFn, FALLING);

  timeVal=0;
}

void loop()
{
  if(state==true){ //카운트 시작
    if(millis()-timeVal>=1000){ //1초단위로 출력
      timeVal=millis();
      millisTime = (millis()-countTime)/1000;
      d1 = millisTime%10; //1의 자리
      d2 = (millisTime/10)%10;//10의 자리
      d3 = (millisTime/100)%10;//100의 자리
      d4 = (millisTime/1000)%10;//1000의 자리

      Serial.print(d4);
      Serial.print(" : ");
      Serial.print(d3);
      Serial.print(" : ");
      Serial.print(d2);
      Serial.print(" : ");
      Serial.println(d1);       
    }
  } 
}

void switchFn(){
  state=!state; 
  countTime=millis();
  timeVal=countTime;
}

3. 결과


  if(state==true){
    if(millis()-timeVal>=100){
      timeVal=millis();
      millisTime = (millis()-countTime)/100;
            ...생략
    }
  }

1초 단위는 너무 길기 때문에 0.1초 단위로 카운트를 하였습니다. 아래 움짤을 보시면 스위치를 누르면 누른 시점에서 카운트가 시작하고 다시 누르면 그상태에서 정지됩니다. 마지막 카운트한 숫자를 확인이 가능합니다 .뭔가를 측정하기에는 좋겠죠. 그리고, 다시 스위치를 누르면 처음부터 카우트를 시작합니다. 위에 공개회로도를 링크 걸어놓았으니깐 가셔서 한번 실험해보세요. 공개회로도에는 1초로 되어 있고요. 사이트 회원가입이 된 분들은 복사해서 시간값을 바꿔서 돌려보세요. 회원가입이 안된 분들은 그냥 1초 단위로 테스트를 하셔야 할꺼에요.


4. 타이머 리셋 시킨 후 카운트



지금까지 설명한 코딩에서는 카운트를 하기 위해서

  • 현재시간 - 스위치누르시간 = 카운트 시작시간
    millisTime = (millis()-countTime)/1000;

위와 같은 식으로 카운트 시작하였습니다. 다른 방법으로는 타이머변수를 리셋시켜 처음 0부터 사작하면 어떨까 하는 생각에 타이머 리셋 post의 복습차원으로 내용을 추가해 봤습니다. 오늘 코딩한 부분에서 변수하나만 외부 변수로 빼내면 간단히 수정됩니다.

extern volatile unsigned long timer0_millis; //타이머변수
리셋 : timer0_millis = 0;

위 코딩을 하면 간단히 리셋이 되고 타이머는 0부터 시작합니다.

const int interruptPin = 2;//인터럽트핀
extern volatile unsigned long timer0_millis; //타이머변수
unsigned long timeVal = 0; //이전시간
unsigned long millisTime = 0; //현재시간

int d1, d2, d3, d4;//자리 숫자
boolean state = false;//타이머 동작 제어 

void setup()
{
  Serial.begin(9600);
  pinMode(interruptPin, INPUT_PULLUP); 
  attachInterrupt(digitalPinToInterrupt(interruptPin), switchFn, FALLING);

  timeVal=0;
}

void loop()
{
  if(state==true){ //카운트 시작
    if(millis()-timeVal>=1000){ //1초단위로 시작
      timeVal=millis();
      millisTime = millis()/1000;
      d1 = millisTime%10; //1의 자리
      d2 = (millisTime/10)%10;//10의 자리
      d3 = (millisTime/100)%10;//100의 자리
      d4 = (millisTime/1000)%10;//1000의 자리

      Serial.print(d4);
      Serial.print(" : ");
      Serial.print(d3);
      Serial.print(" : ");
      Serial.print(d2);
      Serial.print(" : ");
      Serial.println(d1);       
    }
  } 
}

void switchFn(){
  state=!state;
  timer0_millis=0; //타이머변수 리셋
  timeVal=0;
}

현재시간에서 타이머를 누른 시간을 빼줄 필요가 없습니다.

millisTime = millis()/1000;

이렇게 바로 현재시간을 기준으로 카운트하면 됩니다.

void switchFn(){  
  timer0_millis=0; //타이머변수 리셋
  timeVal=0;
}

이렇게 스위치가 눌러졌을 때 타이머변수를 리셋시키면 됩니다.

[결과]
결과는 타이머변수를 사용하지 않는 방식과 같은 결과로 출력됩니다.


문제점

타이머변수를 단순히 스위치를 누르면 리셋시키는 동작을 수행한다면 어떤 문제가 발생할까요. 인터럽트 스위치버턴이기 때문에 사실상 loop()함수가 millis()함수를 읽을 준비를 한 상태라면 그때 리셋을 시키면 결과는 전부 0으로 출력됩니다. maillis()함수를 사용중인데 타이머변수를 리셋하면 약간은 문제가 생길 수 있네요. 그 부분은 또 예외조건을 만들어서 해결하거나 인터렵트 스위치버턴을 사용하지 않고 일반 스위치 버턴을 사용하면 해결 되긴 합니다. 따로 조건을 만들거나 아니면 일반 스위치버턴으로 해서 중간에 millis()함수를 수행 중에 리셋이 되지 않게 처리하면 됩니다. 그 부분은 여러분들이 한번 상상해서 해결해 보세요.

마무리


오늘은 타이머 millis()함수를 사용하여 카운트를 하는 코딩을 실험해 보았습니다. 이틀 동안 millis()함수 하나만을 계속 이야기 하고 있는데 지금 코딩 트레이닝 연습입니다. millis()함수로 아두이노의 현재 타이머 시간값을 얻는다는 개념을 가지고 계속 상상하고 그것을 표현하고 있는데 이 함수만 제대로 상상하시면 많은 것들을 응용할 수 있으니깐 잘 따라오시기 바랍니다.

사실 아두이노에서 1초씩 카운트 한다면 간단히 아래처럼 표현하면 끝납니다.

void loop(){
  if(state==true){
    Serial.println(val);
    delay(1000);
    val++;
  }
}
void switchFn(){  
  state=!state;
  val=0;
}

val을 1씩 증가하면 코딩은 끝납니다. 이 경우는 delay()함수가 도는 1초 동안은 아두이노는 대기상태로 아무동작도 수행하지 않습니다. 그렇기 때문에 여러가지 부품을 혼합할 경우는 millis()함수를 사용하여 시간을 쪼개서 아래와 같이 접근하게 됩니다.

void loop(){
  if(state==true){
   if(millis()-timeVal>=1000){
       Serial.println(val);
       Val++;
       timeVal=millis();
   }
  }
    작업1;
    작업2;
    ...
}
void switchFn(){  
  state=!state;
  timeVal=millis();
  val=0;
}

이렇게 해서 1초 단위로 출력되는 데 1초 동안 대기상태가 되는게 아니라 1초동안 다른 작업들을 계속 수행하게 됩니다. 그런데 오늘 post 코딩은 마무리에서 설명한 코딩으로 간단히 post하지 왜! 복잡하게 지난시간에 자릿수 별로 숫자를 쪼개는 코딩 소스를 기반으로 이런 설명을 할까 하고 생각하실 수 있습니다. 그 이유는 나중에 7-Digit Segment Display를 다루기 위해서 숫자를 쪼갤 수 있는 코딩과 타이머시간값에 대한 이해가 필요하기 때문에 좀 복잡한 코딩으로 설명을 하게 되었습니다.

댓글()