분류 전체보기에 해당하는 글 219

[아두이노] 4-Digit 7-Segment Display 제어

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

[아두이노] 4-Digit 7-Segment Display 제어



지난시간에 2-Digit 7-Segment Display(애노드형)을 가상시뮬레이터에서 실험 했었습니다. 오늘은 실제로 4-Digit 7-Segment Display로 숫자를 출력해보는 실험을 해보겠습니다. 4-Digit 7-Segment Display 부품은 캐소드형이고 지난시간에 일부 변수값과 상태값은 전부 반전된 형태로 표현해야 하기 때문에 회로도를 만들고나서 코딩을 할 때 혼동하지 말고 주의해서 코딩하시기 바랍니다.


1. 4-Digit 7-Segment Display(캐소드형)


4-Digit 7-Segment Display 부품은 아래 이미지와 같습니다. 대충 그렸는데 썩 마음에 들지 않네요.


내부 회로도를 살펴보면 지난 시간에 애노드형 2-Digit 7-Segment Display 회로도를 보셨을 꺼에요. 오늘은 실제 사용되는 4-Digit 7-Segment Display 부품은 캐소드형으로 아래와 같은 회로도로 구성되어 있습니다.


4개의 7-Segment가 a,b,c,d,e,f,g,dp 핀을 공유하고 제어는 D1, D2, D3, D4로 각 D핀을 개방함으로써 해당 7-Segment에 숫자나 문자를 출력하게 됩니다.

캐소드형이기 때문에 D1, D2, D3, D4 핀은 LOW가 될때 a,b,c,d,e,f,g,dp 핀이 HIGH일 때 해당 LED에 불이 들어오게 됩니다. 지난시간의 애노드형 2-Digit 7-Segment Display 반대입니다.

2. 4-Digit 7-Segment Display 핀 번호



위 표를 꼭 기억해 주세요. 4-Digit 7-Segment Display 부품의 위의 부품이미지처럼 순서대로 1~12번까지 있는데 각 핀에 해당된 값을 기억해 주세요. 세개의 표가 있는데 양쪽 표는 여러분들이 보기 편한 것을 참조해 주시고요. 가운데 표는 아두이노와 연결했을 때의 표입니다. 아두이노 핀은 여러분들이 실제 4-Digit 7-Segment Display 부품의 핀을 아두이노에 어떤식으로 연결하느냐에 따라서 아두이노핀 번호는 달라집니다. 가운데 표는 여러분들이 실제 구현을 할 때 표로 직접 작성해주세요. 그리고 나서 코딩을 할 때 해당 4-Digit 7-Segment Display 핀의 값을 기준으로 변수를 선언하실 수 있습니다.

즉, 위 표와 같이 연결을 한다면,

byte segPin[8]={7,3,A3,A1,A0,6,A4,A2}; //사용핀{a,b,c,d,e,f,g,dp} 순서대로임
byte digitPin[4] = {8,5,4,A5}; //segment 위치 핀

이런식으로 pin번호를 순서대로 4-Digit 7-Segment Display 핀값에 매칭되는 아두이노 핀번호를 선언해주면 됩니다. 위 배열 변수에 저장된 값은 4-Digit 7-Segment Display부품과 아두이노에 어떻게 연결했느냐에 따라서 값이 달라집니다.

여러분들이 편한 방식으로 마음대로 연결하시고 코딩할 때만 핀변수값을 정확히 선언하시면 부품을 제어하는데 어려움은 없을 거라 생각됩니다.

참고로, 선 연결 할 때 a,b,d,c,e,f,g,dp핀을 한쪽으로 뭉쳐서 순서대로 아두이노에 연결하고 나머지 4개의 핀은 d1,d2,d3,d4에 순서대로 연결하셔도 됩니다. 하지만 이경우는 4-Digit 7-Segment Display 부품의 핀 위치값을 정확히 알고 있을 때 선을 연결하는 방법인데 좀 지져분하게 선이 연결 됩니다. 그리고, 선 연결 할 때 실수 할 가능성이 가장 큽니다. 별로 추천드리지 않습니다.

쉽게, 4-Digit 7-Segment Display 부품 핀번호 1~12번을 아두이노 2~12번으로 순서대로 매칭시켜서 선을 연결하거나 아니면 아두이노의 아날로그와 디지털핀을 이용하여 4-Digit 7-Segment Display 부품 핀 번호 1~6번은 아두이노 A0~A5에 연결하고 나머지 7~12번은 아두이노 2~7번으로 위아래 순서대로 선을 연결하셔도 됩니다. 그냥 순서대로 핀을 보기 좋게 연결하시고 코딩에서 해당핀에 대한 변수만 제대로 선언하시면 제어하는데 어려움이 없을 꺼에요

2. 4-Digit 7-Segment Display 회로도


준비물 : 4-Digit 7-Segment Display 1개, 아두이노우노
내용 : 4-Digit 7-Segment Display 부품의 핀 번호 순서에 맞게 아두이노우노에 연결 하시오.



위 회로도는 4-Digit 7-Segment Display 핀번호 1~6번은 아날로그 핀 A0~A5에 순서대로 연결하고 7~12번은 순서대로 2~7번으로 연결하려다가 따로 스위치버턴을 인터럽트 핀을 이용할 까 고민중이라 인터럽트핀이 2,3 번 핀 중에 우선 2번핀 하나정도는 남겨 두기 위해서 3~8번으로 순서대로 핀을 연결했네요.

핀 연결은 여러분들이 편하신 방법으로 연결하시면 됩니다. 규칙도 없고 그냥 여러분이 원하는 스타일로 연결만 해주세요. 연결하시고 나서 코딩을 할 때 해당핀에 대한 변수를 선언할 때 정확히 지정만 해주면 됩니다.

2. 코딩


1)LED 숫자 패턴


[애노드형]

//a,b,c,d,e,f,g
byte segValue[10][7] = {
   {0,0,0,0,0,0,1}, //0
   {1,0,0,1,1,1,1}, //1
   {0,0,1,0,0,1,0}, //2
   {0,0,0,0,1,1,0}, //3
   {1,0,0,1,1,0,0}, //4
   {0,1,0,0,1,0,0}, //5
   {0,1,0,0,0,0,0}, //6
   {0,0,0,1,1,1,1}, //7
   {0,0,0,0,0,0,0}, //8
   {0,0,0,0,1,0,0}  //9  
};

캐소드형 회로도이기 때문에 아래와 같이 1은 0으로 0은 1로 변경만 시키면 됩니다.

//a,b,c,d,e,f,g 상태값
byte segValue[10][7] = {
   {1,1,1,1,1,1,0}, //0
   {0,1,1,0,0,0,0}, //1
   {1,1,0,1,1,0,1}, //2
   {1,1,1,1,0,0,1}, //3
   {0,1,1,0,0,1,1}, //4
   {1,0,1,1,0,1,1}, //5
   {1,0,1,1,1,1,1}, //6
   {1,1,1,0,0,0,0}, //7
   {1,1,1,1,1,1,1}, //8
   {1,1,1,1,0,1,1}  //9  
};

2) 숫자 카운트 (0~9999)


아래와 같이 millis()함수는 실시간으로 현재 타이머시간값을 읽어옵니다 그 값을 기준으로 각 자릿숫자를 d1,d2,d3,d4에 저장됩니다.

readTime = millis()/1000;
d1 = readTime%10; //1의 자리
d2 = (readTime/10)%10; //10의 자리
d3 = (readTime/100)%10; //100의 자리
d4 = (readTime/1000)%10; //1000의 자리

2-Digit 7-Segment Display 에서 100의 자리, 1000의 자리가 추가 되었을 뿐 따로 코딩에는 변동이 없습니다.

2) 숫자 카운트 출력(0~9999)

segOutput(3,d1,0); //1의 자리
if(readTime>=10) segOutput(2,d2,0); //10의 자리  
if(readTime>=100) segOutput(1,d3,0); //100의 자리  
if(readTime>=1000) segOutput(0,d4,0); //1000의 자리  

출력되 100의 자리, 1000의 자리로 segOutput()함수 인자만 맞게 넣어주시면 4자리 숫자가 카운트 되어 출력됩니다.

3) 종합소스


//a,b,c,d,e,f,g 상태값
byte segValue[10][7] = {
   {1,1,1,1,1,1,0}, //0
   {0,1,1,0,0,0,0}, //1
   {1,1,0,1,1,0,1}, //2
   {1,1,1,1,0,0,1}, //3
   {0,1,1,0,0,1,1}, //4
   {1,0,1,1,0,1,1}, //5
   {1,0,1,1,1,1,1}, //6
   {1,1,1,0,0,0,0}, //7
   {1,1,1,1,1,1,1}, //8
   {1,1,1,1,0,1,1}  //9  
};

byte segPin[8]={7,3,A3,A1,A0,6,A4,A2}; //사용핀{a,b,c,d,e,f,g,dp} 순서대로임
byte digitPin[4] = {8,5,4,A5}; //segment 위치 핀

unsigned long readTime=0; //현재시간
int d1 = 0; //1의 자리
int d2 = 0; //10의 자리
int d3 = 0; //100의 자리
int d4 = 0; //1000의 자리

void setup() {
  for(int i=0;i<10;i++){
    pinMode(segPin[i], OUTPUT);
  }
  for(int j=0;j<4;j++){
    pinMode(digitPin[j], OUTPUT);   
    digitalWrite(digitPin[j], HIGH); 
  }
}

void loop() {   
  readTime = millis()/1000;
  d1 = readTime%10; //1의 자리
  d2 = (readTime/10)%10; //10의 자리
  d3 = (readTime/100)%10; //100의 자리
  d4 = (readTime/1000)%10; //1000의 자리
  
  segOutput(3,d1,0); //1의 자리
  if(readTime>=10) segOutput(2,d2,0); //10의 자리  
  if(readTime>=100) segOutput(1,d3,0); //10의 자리  
  if(readTime>=1000) segOutput(0,d4,0); //10의 자리  
}
//LED 초기화
void segClear(){ 
  for(int i=0;i<8;i++){
    digitalWrite(segPin[i], LOW);        
  }
}
//LED 출력
void segOutput(int d, int Number, int dp){ 
  segClear();
  digitalWrite(digitPin[d], LOW); 
  for(int i=0;i<7;i++){
     digitalWrite(segPin[i], segValue[Number][i]);        
  }
  digitalWrite(segPin[7], dp);
  delayMicroseconds(1000);
  digitalWrite(digitPin[d], HIGH); 
}

3. 결과


동영상 결과는 사실 1초 단위로 카운트 할려고 했는데 사실 4개의 7-Segment의 숫자가 정확히 출력되는지 확인하기 위해서는 1000초가 걸립니다. 즉, 16분 40초를 기다려야 합니다. 그래서 0.01초로 카운트를 한 영상입니다.

<
readTime = millis()/10; //0.01초 단위






4. 입터럽트 스위치 버턴으로 타이머 리셋


스위치버턴을 입터럽트 이벤트를 발생시킬 예정입니다. 스톱워치처럼 스위치를 누르면 타이머가 누른 시점에서 리셋되어 돌아가고 다시 스위치를 누르면 정지되고 타이머가 돌다가 멈춘 시간값을 보여주게 됩니다. 그리고 다시 스위치를 누르면 처음부터 다시 타이머가 돌게하는 실험을 할 예정입니다.

1) 회로도


  • 준비물 : 4-Digit 7-Segment Display 1개, 스위치버턴 1개, 아두이노우노
  • 내용 : 4-Digit 7-Segment Display 부품의 핀 번호 순서에 맞게 아두이노우노에 연결하고, 스위치 버턴은 2번핀에 연결하시오.

2번핀은 인터럽트 핀으로 스위치 버턴을 2번핀에 연결했습니다.


2) 코딩



인터럽트스위치 버턴을 사용하니깐 아래와 같이 코딩합니다 사전학습에 있던 내용이깐 가셔서 읽고 오세요.

const byte interruptPin = 2;//인터럽트핀
void setup() {
  pinMode(interruptPin, INPUT_PULLUP); 
  attachInterrupt(digitalPinToInterrupt(interruptPin), switchFn, FALLING);
}
void loop() {   
  if(state==true){ //스위치 상태가 true일때 카운트
        스위치눌러질때 동작명령;
    }
}
void switchFn(){
  state=!state;
  if(state==true){
    timer0_millis=0; //타이머변수 리셋  
  }
}

여기서, switchFn() 함수를 약간 수정했는데 그것은 state의 값이 true일 때만 timer0_millis을 0으로 초기화 했습니다. 즉, 스위치눌러 타이버가 동작할 때 그 시점에 timer0_millis=0으로 초기화 한다는 의미입니다.

위에서 타이머 동작 기본 소스에서 인터럽트 스위치 버턴 코딩을 합치면 아래와 같은 전체 소스가 완성 됩니다.

//a,b,c,d,e,f,g 상태값
const byte segValue[10][7] = {
   {1,1,1,1,1,1,0}, //0
   {0,1,1,0,0,0,0}, //1
   {1,1,0,1,1,0,1}, //2
   {1,1,1,1,0,0,1}, //3
   {0,1,1,0,0,1,1}, //4
   {1,0,1,1,0,1,1}, //5
   {1,0,1,1,1,1,1}, //6
   {1,1,1,0,0,0,0}, //7
   {1,1,1,1,1,1,1}, //8
   {1,1,1,1,0,1,1}  //9  
};

const byte segPin[8]={7,3,A3,A1,A0,6,A4,A2}; //사용핀{a,b,c,d,e,f,g,dp} 순서대로임
const byte digitPin[4] = {8,5,4,A5}; //segment 위치 핀

const byte interruptPin = 2;//인터럽트핀
extern volatile unsigned long timer0_millis; //타이머변수

boolean state = false;//타이머 동작 제어

unsigned long readTime=0; //현재시간
int d1 = 0; //1의 자리
int d2 = 0; //10의 자리
int d3 = 0; //100의 자리
int d4 = 0; //1000의 자리

void setup() {
  pinMode(interruptPin, INPUT_PULLUP); 
  attachInterrupt(digitalPinToInterrupt(interruptPin), switchFn, FALLING);
  
  for(int i=0;i<10;i++){
    pinMode(segPin[i], OUTPUT);
  }
  for(int j=0;j<4;j++){
    pinMode(digitPin[j], OUTPUT);    
    digitalWrite(digitPin[j], HIGH); 
  }
}

void loop() {   
  if(state==true){ //스위치 상태가 true일때 카운트
    readTime = millis()/1000;
    d1 = readTime%10; //1의 자리
    d2 = (readTime/10)%10; //10의 자리
    d3 = (readTime/100)%10; //100의 자리
    d4 = (readTime/1000)%10; //1000의 자리 
  }
  if(readTime>=0)segOutput(3,d1,0); //1의 자리
  if(readTime>=10) segOutput(2,d2,0); //10의 자리  
  if(readTime>=100) segOutput(1,d3,0); //10의 자리  
  if(readTime>=1000) segOutput(0,d4,0); //10의 자리 
}
//스위치버턴 이벤트
void switchFn(){
  state=!state;
  if(state==true){
    timer0_millis=0; //타이머변수 리셋  
  }
}
//LED 초기화
void segClear(){ 
  for(int i=0;i<8;i++){
    digitalWrite(segPin[i], LOW);        
  }
}
//LED 출력
void segOutput(int d, int Number, int dp){ 
  segClear();
  digitalWrite(digitPin[d], LOW); 
  for(int i=0;i<7;i++){
     digitalWrite(segPin[i], segValue[Number][i]);        
  }
  digitalWrite(segPin[7], dp);
  delayMicroseconds(1000);
  digitalWrite(digitPin[d], HIGH); 
}

3) 결과


스위치가 눌러지면 state=true가 되고 1초 단위로 카운트 하게 됩니다. 참고로 다시 누르면 state=false로 정지됩니다. 다시 누르면 state=true가 되고 timer0_millis=0으로 초기화 되고 처음부터 다시 1초 단위로 카운트를 하게 됩니다.


마무리


오늘은 실제 4-Digit 7-Segment Display 부품에 카운트 결과를 출력해 보았습니다. 지난 시간에 시리얼모니터로 결과를 출력했던 것과 다르게 좀 복잡해 보였을 꺼에요. 하나하나 뜯어보면 어렵지 않는 내용이니깐 분리해서 하나씩 살펴보시기 바랍니다.

다음 시간에는 아두이노 시계로 4-Digit 7-Segment Display에 현재 시간을 만들고 그 시간을 기준으로 시간이 흘러가는 것을 실험해 보겠습니다.

댓글()

[아두이노] 2-Digit 7-Segment Display

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

[아두이노] 2-Digit 7-Segment Display



지난 시간까지 해서 시간에 대해서 여러가지 원리에 대해서 살펴 보았습니다. 오늘은 7-Segment Display을 이용하여 지난 시간에 공부했던 코딩 결과를 출력해보는 시간을 갖도록 하죠. 원래는 4-Digit 7-Segment Display를 회로도를 만들어서 실험한 결과를 보여드릴려고 했는데 회로도를 너무 복잡하고 지져분하게 표현하면 가독성이 떨어지기 때문에 간단히 2개로 2-Digit 7-Segment Display로 구성하고 출력 원리만 설명하면 될 것 같아서 회로도를 최소화 했습니다. 실제 실험을 하신다면 이 원리를 기반으로 4-Digit 7-Segment Display로 실험하셔도 됩니다.


1. 7-Segment 복습



너무 오래전에 이야기 했던 거라 간단히 복습차원으로 설명할께요. 각 post 연계를 하면서도 해당 post는 독립적인 내용을 담고 있어야 해서 어쩔 수 없이 중복된 복습 내용을 간단히 소개하도록 하게 합니다.


위 그림처럼 a,b,c,d,e,f,g,dp 핀과 com1,2핀으로 기본 구성되어 있습니다.


  • 애노드 : com1,2 중 하나에 Vcc가 공급되고 나머지 a,b,c,d,e,f,g,dp에 0V(gnd)상태가 되면 LED에 불이 들어옴
  • 캐소드 : com1,2 중 하나에 Gnd에 연결되면 나미저 a,b,c,d,e,f,g,dp에 5V(Vcc)상태가 되면 LED에 불이 들어옴

이 두 개념만 가지고 있으면 됩니다. LED은 +,-가 연결되어 불이 들어오기 때문에 7-Segment도 한쪽이 +면 반대쪽이 -가 되어야 불이 들어옵니다. 어떤 원리인지 아시겠지요.

참고로, 실험은 애노드 2-Digit 7-Segment Display 회로도로 구성했습니다. 실제로 4-Digit 7-Segment Display를 구매 하시면 캐소드 부품으로 실험 하실 꺼에요. 혹시, 자신이 쓰는 부품이 다를 수 있으니 부품에 대한 데이터시트를 꼭 확인하시고 실험하세요. 왜! 불이 안들어오지 하는 이유가 대부분 핀 연결을 반대로 하는 경우가 대부분입니다. 그리고, 왜! 숫자가 정상적으로 안나오지 하면 애노드와 캐소드는 a,b,c,d,e,f,g,dp 핀 값이 반대 값을 갖기 때문에 반대 LED에 불이 들어오는 경우입니다. 꼭! 제대로 확인하시고 실험하시기 바랍니다.

2. 2-Digit 7-Segment Display 원리(애노드형)


2-Digit 7-Segment Display or 4-Digit 7-Segment Display 의 원리는 동일합니다. 같은 a,b,c,d,e,f,g,dp핀을 공유하고 Vcc핀은 각각의 7-Segment의 전원을 공급하는 별도 핀으로 나눠주면 2개든 4개든 상관없이 동시에 제어가 가능합니다.


위 그림처럼 각각 D1의 7-Segment와 D2의 7-Segment에 HIGH 되면 a,b,c,d,e,f,g,dp핀이 LOW가 되면 LED에 불이 들어오는데 D1, D2가 동시에 HIGH되면 2개의 7-Segment에는 동일한 문자가 출력됩니다. 2개의 7-Segment에 서로 다른 문자가 출력되게 할려면 각 D1, D2의 전원 공급의 시간차를 두고 a,b,c,d,e,f,g,dp핀을 제어하면 됩니다. 즉, 전류 공급에 의한 일시적 잔상효과를 이용한 방법이지요.

두개의 7-Segment에 시간차 전원 공급으로 문자를 출력하게 함으로서 서로 다른 문자를 출력할 수 있게 됩니다. 여기서, 주의할 점은 시간차의 값이 크면 동시에 출력되는 효과를 볼 수 없으며 시간차를 최소화 하여 잔상효과로 동시에 2개의 7-Segment에 문자가 출력되는 듯한 착시효과가 나타나게 시간차를 잘 조절하셔야 합니다.

//D1 출력
digitalWrite(D1, HIGH); 
for(int i=0;i<7;i++){
   digitalWrite(segPin[i], D1문자LED값);        
}
digitalWrite(segPin[7], dp);
delayMicroseconds(1000);
digitalWrite(D1, LOW); 

//D2 출력
digitalWrite(D2, HIGH); 
for(int i=0;i<7;i++){
   digitalWrite(segPin[i], D2문자LED값);        
}
digitalWrite(segPin[7], dp);
delayMicroseconds(1000);
digitalWrite(D2, LOW); 

이렇게 delayMicroseconds(1000)의 시간만큼 D1, D2 LED에 전원을 공급하게 됩니다. 이 명령문이 교대로 loop()함수로 반복하면 D1과 D2가 동시에 문자를 출력하는 효과가 발생합니다. (1초는 100만 마이크로 초)

참고로, 짧은 시간차를 이용하기 위해서 delay()함수 대신 delayMicroseconds()함수를 사용했네요.

같은 a,b,c,d,e,f,g,dp핀을 공유하면서 D1, D2의 전원 공급의 시간차로 서로 다른 문자를 출력할 수 있게 됩니다. 만약에 4-Digit 7-Segment Display 이면 D1, D2, D3, D4로 각각 시간차를 두고 전원을 공급한다면 4개의 문자가 동시에 출력되는 듯한 착시효과로 동시에 4개의 서로다른 문자를 출력하는 것 같은 결과를 얻게 됩니다.

3. 2-Digit 7-Segment Display 회로도


  • 준비물 : 7-Segment 2개, 330옴 2개, 아두이노우노
  • 내용 : 애노드형 방식으로 핀은 연결하는데 a,b,c,d,e,f,g,dp은 원하는 디지털핀에 연결하시오.


혹시, 위 그림이 복잡해 보이시면 사전학습 Post [아두이노] 7 Segment LED 제어 의 글을 보시면 애노드형으로 한개짜리 7-Segment로 구성된 회로도가 있을 꺼에요. 그걸 보시고 한개 더 중복해서 연결하시면 됩니다.

4. 숫자 카운트 코딩(0~99)


위의 2-Digit 7-Segment Display 원리의 코딩을 기반으로 숫자를 카운트 해 볼까요.

1) LED 숫자 패턴 만들기


//a,b,c,d,e,f,g
byte segValue[10][7] = { //애노드형
   {0,0,0,0,0,0,1}, //0
   {1,0,0,1,1,1,1}, //1
   {0,0,1,0,0,1,0}, //2
   {0,0,0,0,1,1,0}, //3
   {1,0,0,1,1,0,0}, //4
   {0,1,0,0,1,0,0}, //5
   {0,1,0,0,0,0,0}, //6
   {0,0,0,1,1,1,1}, //7
   {0,0,0,0,0,0,0}, //8
   {0,0,0,0,1,0,0}  //9  
};

D1의 숫자를 만든다면 D1이 Vcc면 a,b,c,d,e,f,g의 핀이 0일때 불이 들어옵니다

0의 숫자를 만들려면 LED에 불은 g핀을 제외한 나머지가 다 0이 되어야 합니다.

0 => 0,0,0,0,0,0,1

나머지 숫자도 이런식으로 만들면 됩니다, 참고로 저장변수공간을 줄일려면 아예 7개의 값을 byte형으로 만드셔도 됩니다.

0 => 0,0,0,0,0,0,1 =>OB00000010

이렇게 해서 10개의 byte 배열변수로 만들면 저장공간을 대폭 줄일 수 있습니다. 이해를 돕기 위해서 그냥 개별적으로 저장공간에 하나의 상태값만 저장되게 코딩하겠습니다. 'OB00000010'으로 표현하면 bitRead()함수로 각각 해당 위치의 값을 읽어와서 해당 LED에 불이 들어오게 제어하는 코딩을 하면 되는데 이 부분은 여러분들에게 숙제로 남겨두겠습니다.

2) millis()함수로 시간값 쪼개기


시간 함수에 대해서 지난 시간에 꽤 오래 동안 몇일에 걸쳐 이야기를 했기 때문에 생략하고 간단히 코딩으로 소개하는 수준으로 넘어가겠습니다.

readTime = millis()/1000; //초단위로 만듬
d1 = readTime%10; //1의 자리
d2 = (readTime/10)%10; //10의 자리

millis()함수로 읽은 타이머 시간값을 1000으로 나눠서 초단위로 시간값으로 readTime변수에 저장합니다. 이 초 값을 지난시간에 배웠던 방식으로 1의 자리와 10의 자리로 분리해 내서 d1, d2의 해당 숫자를 저장합니다.

3) 숫자 출력


위 2-Digit 7-Segment Display 원리에서 Segment의 숫자를 출력하기 위해서 코딩부분을 따로 외부 사용자정의 함수로 빼서 아래와 같이 segOutput()함수로 만들었습니다. d은 digit 위치이고, Number은 출력되는 숫자, dp은 Segment의 왼쪽 사이드에 있는 dot LED의 상태값입니다.

//LED 출력
void segOutput(int d, int Number, int dp){ 
  segClear();
  digitalWrite(digitPin[d], HIGH); 
  for(int i=0;i<7;i++){
     digitalWrite(segPin[i], segValue[Number][i]);        
  }
  digitalWrite(segPin[7], dp);
  delayMicroseconds(1000);
  digitalWrite(digitPin[d], LOW); 
}

코딩에 보시면 segClear()함수가 있는데 이것 역시 사용자 정의 함수로 제가 만든 함수입니다. 함수단어명을 유심히 보면 Segment를 뭔가 Clear한다는 의미를 담고 있는 걸 보실 수 있을거에요. 즉, LED를 초기화 상태로 되돌리는 함수를 하나 더 만들었습니다.

그 이유는, 연속으로 각 Segment에 숫자를 출력하면 한쪽 Segment의 값이 유지된 상태에서 다른 Segment에 LED에 불이 들어오게 할때 기존에 남아 있는 a,b,c,d,e,f,g,dp핀의 상태값이 유지된 상태에서 전류가 일시적으로 공급되어 양쪽 Segment에 일부 핀에 전류가 동시 공급되는 현상이 발생합니다. 그래서 잔상으로 두 Segment의 일부 LED에 불이 겹쳐서 나오게 됩니다. 그렇기 때문에 각 Segment에 불이 들어오게 하기전에 초기화 작업을 해줘야 겹치는 LED의 문제를 해결 할 수 있습니다.

void segClear(){
  for(int i=0;i<8;i++){
    digitalWrite(segPin[i], HIGH);        
  }
}

이렇게 간단하게 a,b,c,d,e,f,g,dp핀 HIGH 상태로 초기화 하시면 됩니다.

4) 전체소스

//a,b,c,d,e,f,g 상태값
byte segValue[10][7] = { //애노드형
   {0,0,0,0,0,0,1}, //0
   {1,0,0,1,1,1,1}, //1
   {0,0,1,0,0,1,0}, //2
   {0,0,0,0,1,1,0}, //3
   {1,0,0,1,1,0,0}, //4
   {0,1,0,0,1,0,0}, //5
   {0,1,0,0,0,0,0}, //6
   {0,0,0,1,1,1,1}, //7
   {0,0,0,0,0,0,0}, //8
   {0,0,0,0,1,0,0}  //9  
};

byte segPin[8]={2,3,4,5,6,7,8,9}; //사용핀{a,b,c,d,e,f,g,dp} 순서대로임
byte digitPin[2] = {A0,A1}; //segment 위치 핀

unsigned long readTime=0; //현재시간
int d1 = 0; //1의 자리
int d2 = 0; //10의 자리

void setup() {
  for(int i=0;i<10;i++){
    pinMode(segPin[i], OUTPUT);
  }
  pinMode(digitPin[0], OUTPUT);
  pinMode(digitPin[1], OUTPUT);    
  digitalWrite(digitPin[0], LOW); 
  digitalWrite(digitPin[1], LOW);      
}

void loop() {  
  //시간 갱신
  readTime = millis()/1000;
  d1 = readTime%10; //1의 자리
  d2 = (readTime/10)%10; //10의 자리
  
  segOutput(1,d1,1); //1의 자리
  if(readTime>=10) segOutput(0,d2,1); //10의 자리  
}
//LED 초기화
void segClear(){ 
  for(int i=0;i<8;i++){
    digitalWrite(segPin[i], HIGH);        
  }
}
//LED 출력
void segOutput(int d, int Number, int dp){ 
  segClear();
  digitalWrite(digitPin[d], HIGH); 
  for(int i=0;i<7;i++){
     digitalWrite(segPin[i], segValue[Number][i]);        
  }
  digitalWrite(segPin[7], dp);
  delayMicroseconds(1000);
  digitalWrite(digitPin[d], LOW); 
}

5. 결과



마무리


2-Digit 7-Segment Display를 만들어 보았습니다. 방식은 애노드형으로 회로도를 구성해 보았는데 지끔까지 아두이오 post에서 한번이상은 거론했던 내용들이라서 이전 post를 보셨던 분들은 복습의 시간이 되었을 듯 싶네요.

오늘의 핵심은,

digitalWrite(digitPin[d], HIGH); 
  for(int i=0;i<7;i++){
     digitalWrite(segPin[i], segValue[Number][i]);        
  }
  digitalWrite(segPin[7], dp);
  delayMicroseconds(1000);
digitalWrite(digitPin[d], LOW); 

위 코딩은 2-Digit 7-Segment Display에 숫자를 출력하기 위한 핵심 코딩입니다. 이부분만 이해하신다면 어떤 숫자나 문자든 쉽게 2-Digit 7-Segment Display에 출력할 수 있게 됩니다.

다시 보시면,

digitalWrite(digitPin[d], HIGH); 
숫자 LED 출력;
delayMicroseconds(1000);
digitalWrite(digitPin[d], LOW); 

이 로직으로 애노드형이기 때문에 digitPin[d]은 특정 위치의 7-Segment이고 전원을 공급 HIGH로 시작합니다. 전원이 공급될 때 출력 LED의 상태 LOW가 결정되고 그 상태를 delayMicroseconds(1000)의 딜레이시간동안 유지합니다. 즉, 해당 숫자가 딜레이시간만큼 전원이 공급된다는 의미이지요. 그리고 나서 digitPin[d] 전원공급 해체 LOW로 하나의 숫자를 7-Segment에 출력시키는 최소 시간의 로직이 됩니다.

이걸 기반으로 여러개의 7-Segment에 숫자들을 출력하여 동시에 출력되는 듯한 착시효과를 부여하게 됩니다. 몇줄 안되는 이 로직을 꼭 기억해 주세요. 이걸 이해하시게 되면 나중에 스톱워치, 시계, 온도계 등 과 같은 여러 숫자값을 출력하는 장치로 활용할 수 있습니다. 지난 시간에 시간에 관한 여러가지 원리를 배웠는데 그 결과를 7-Segment에 출력할 수 있습니다.

한번 오늘 배운 회로도를 가지고 지난 시간에 배웠던 것들을 한번 적용해 보셨으면 합니다. 저도 몇가지 가상과 실제로 실험하여 post를 할 예정입니다. 어떤 값을 출력할지는 아직 결정되지 않았고 아마도 시간 관련해서 출력하는 장치로 실험을 할 것 같습니다. 여러분들도 한번 오늘 배운 내용을 토대로 어떤 부품의 출력장치로 사용할지 상상의 나래를 펼쳐 보세요.


댓글()

[아두이노] 시간 millis() 함수로 시계 코딩

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

[아두이노] 시간 millis() 함수로 시계 코딩



오늘은 millis() 함수를 이용한 시계 코딩을 실험 할 예정입니다. 지난 시간의 초를 시/분/초를 변환하거나 시/분/초를 초로 변환하는 코딩을 실험 하였습니다. 여기에, millis()함수와 전역 타이머변수를 이용하여 아두이노 시계 코딩을 만들어 보겠습니다. 시계 코딩의 결과는 시리얼모니터로 간단히 출력하는 실험이지만 나중에 외부 출력 부품을 이용하여 디지털 시계를 만들거나 아날로그 시계를 만들어 볼 예정입니다. 우선은 시계 코딩으로 어떻게 동작하는지 먼저 만들어 봐야 겠지요.


이제 본격적으로 실험을 해볼까요.

1. 시간



시간은 하루를 기준으로 24시간, 1시간은 60분, 1분은 60초로 이루어져 있습니다. 누구나 다 아는 기본 내용이지요. 시계는 하루 24시간을 표시하는 기계입니다. 즉, 24시간 이상의 시간이 흐르는 다시 리셋되어 시간이 0시부터 흐르게 됩니다. 가령, 25시가 입력되면 다음날 새벽 1시가 되겠죠. 이처럼 몇시간이 입력되었던 24시간을 기준으로 표시됩니다.

표시 => 전체시간 % 24 = 현재시간
예) 25%24 =1

몇시간이 입력되었던 날짜와 상관없이 24시간을 기준으로 표시만 하면 됩니다. 대충 시간은 어떤 느낌으로 출력해야 할지 아시겠지요. "23:59:59"가 되었을 때 1초가 흐르면 바로 "0:0:0"으로 리셋을 시키게 하여 24시간 기준으로 표시되게 하면 되겠죠.

2. 시간 입력



  // 시:분:초 입력
  if(Serial.available()){
    String inString = Serial.readStringUntil('\n');    
    int index1 = inString.indexOf(':'); 
    int index2 = inString.indexOf(':',index1+1);   
    int index3 = inString.length();
    int hour = inString.substring(0, index1).toInt();
    int min = inString.substring(index1+1,index2).toInt();
    int sec = inString.substring(index2+1,index3).toInt();
}

위 코딩은 지난시간에 시리얼모니터로 시/분/초를 입력한 값을 각각 hour, min, sec 변수에 저장하는 코딩입니다. 시간을 입력하고 그 시간문자열에서 시간들을 분리해 냅니다. 위 코딩 설명은 사전학습에서 이야기 했기 때문에 생략합니다.

3. 시간 동작


아두이노는 시간을 어떻게 흐르게 할까요. 아두이노 내부에 타이머가 있고 이 타이머시간값을 millis()함수를 사용하여 타이머시간값을 가져올 수 있고 이 값을 통해서 아두이노 시간을 흐르게 할 수 있습니다. 아두이노는 1초 단위의 시간을 구할 수 있습니다. delay(1000) 하고 시간변수를 +1씩 증가시키면 쉽게 1초단위로 시간값을 구할 수 있지만, 다른 부품들과 연동하기 위해서는 delay()함수를 될 수 있으면 쓰면 안됩니다. 그래서, millis()함수를 사용하는 데 다음 아래와 같이 코딩하면 됩니다.

if(millis()-timeVal>=1000){
  sec++; //1초증가
}

여기서, 시간을 외부로 부터 입력받아 세팅한 뒤에 시간이 흘러가게 해야 한다면 위 명령문에서 1초 증가를 변수에다 저장하여 시간을 돌리면 초, 분, 시 기준으로 아래 if문을 만들어 시간값이 증가하도록 코딩을 하면 됩니다.

if(sec==60){
  sec=0;
  min++; //1분증가
}
if(min==60){
  min=0;
  hour++; //1시간증가
}
if(hour==24){
  hour=0;  
}

이렇게 3개의 if문을 만들어서 시간값을 만들어 낼 수 있지만 이 방법으로 하지 않을 겁니다. 다른 방식으로 타이머변수에 현재시간으로 초기화하는 방법으로 코딩하겠습니다.

extern volatile unsigned long timer0_millis; //타이머변수
timer0_millis = ((long)hour*3600+min*60+sec)*1000;

이렇게, 시리얼모니터에서 시간을 입력 받은 hour, min, sec값을 초로 변환 한 값을 timer0_millis값으로 초기화 하면 어떤 현상이 발생할까요. timer0_millis 시간을 기준으로 타이머는 계속 흘러가기 때문에 millis()함수로 읽은 시간값은 입력된 시간을 기준으로 시간이 흘러가게 됩니다. 그렇게 되면 지난시간에 배운 millis()함수 값을 읽어서 시/분/초로 변환만 하면 현재 시간을 실시간으로 얻을 수 있습니다.

readTime = millis()/1000;
      
sec = readTime%60;
min = (readTime/60)%60;
hour = (readTime/(60*60))%24; 

이렇게 하면 시간을 알 수 있게 됩니다. 이게 바로 아두이노 시계가 되는 것이죠. 위의 if문으로 시간을 1초씩 증가해서 초, 분, 시를 증가시키든 방금했던 방식으로 아예 timer0_millis을 현재 시간으로 초기화 할지는 여러분이 선택만 하시면 됩니다.

두번째 timer0_millis변수로 초기화 하는 방법을 취하고 24시간 기준으로 timer0_millis를 리셋시키는 방법으로 코딩을 수정하면 다음과 같은 코딩이 됩니다.

readTime = millis()/1000;
      
if(millis()>=86400000){ //24시간 초기화
  timer0_millis=0;
}
sec = readTime%60;
min = (readTime/60)%60;
hour = (readTime/(60*60))%24; 

millis() 현재 시계의 시간값으로 타이머가 돌고 있습니다. 그 값이 24시간이 되면 0시로 리셋이 되어야 합니다.

1일 24시간 => 1x60x60x24 = 86400 x 1000(1초) = 86400000

위처럼, millis() 시간값이 86400000이 되면 1일이 됩니다. 24시간이라는 소리죠. 그러면 0시로 리셋을 해야 합니다. 간단히, timer0_millis 변수를 0으로 초기화 하면 0시부터 타이머가 돌게 됩니다. 이렇게 하면 24시간 기준으로 무한으로 아두이노 시계가 돌게 되겠죠.

그냥, 계속 millis()함수로 읽으면 되지 하실 분도 있을거에요. timer0_millis변수는 unsigned long 자료형입니다. 수치를 저장하는 값이 한계가 있습니다. 한달 조금 지나면 리셋이 되고 사실 그전에 시간에 대해 정확히 리셋 시점을 잡아줘야 합니다. 안그러면 리셋되는 시점에서 시간에 문제가 생길 수 있습니다. 편하게 24시간 기준으로 리셋을 시키는 것이 좋겠죠. 리셋 시점은 여러분들의 자유입니다.

4. 종합 소스


extern volatile unsigned long timer0_millis; //타이머변수
unsigned long timeVal; //이전시간
unsigned long readTime; //현재타이머시간
int hour, min, sec;
boolean state=false;

void setup()
{
  Serial.begin(9600);  
}

void loop()
{ 
  if(Serial.available()){
    String inString = Serial.readStringUntil('\n');    
    int index1 = inString.indexOf(':'); 
    int index2 = inString.indexOf(':',index1+1);   
    int index3 = inString.length();
    
    hour = inString.substring(0, index1).toInt();
    min = inString.substring(index1+1,index2).toInt();
    sec = inString.substring(index2+1,index3).toInt();
  
    
    timer0_millis = ((long)hour*3600+min*60+sec)*1000;
    state=true;
    timeVal=millis();
  }
  if(state==true){ //시리얼모니털 출력 시작
    
    if(millis()-timeVal>=1000){ //1초 단위 출력
     readTime = millis()/1000;
      
     if(millis()>=86400000){
       timer0_millis=0;
     }
     timeVal = millis();
   
     sec = readTime%60;
     min = (readTime/60)%60;
     hour = (readTime/(60*60))%24;       
      
     Serial.print(hour);
     Serial.print(" : ");
     Serial.print(min);
     Serial.print(" : ");
     Serial.println(sec);      
    }
  }
}

위 소스에서는 1초 단위로 출력하기 위해서

if(millis()-timeVal>=1000){ //1초 단위 출력
 timeVal = millis();
}

이 명령문을 사용했습니다. 이것은 1초 단위로 시리얼모니터로 출력하기 위한 락을 걸어놓은 느낌의 조건문입니다. 1초가 되었을 때 시간 값을 시리얼모니터로 출력을 할꺼야 하는 조건문으로 생각하세요. 위에서 설명했던 각각의 내용들을 전체 소스에 배치한 것 뿐이니깐 혹시 이해 안되면 위 설명을 다시 보시기 바랍니다.

5. 결과


시리얼모니터로 시간값을 입력하면 그 시간을 기준으로 타이머가 계속 흘러간다. 그래서 시계처럼 시간을 출력할 수 있다.


6. millis() 함수 대신에 timer0_millis 사용 가능


extern volatile unsigned long timer0_millis; //타이머변수

이렇게 선언하게 되면 구지 millis()함수를 사용안하고 timer0_millis로 시간값을 가져올 수 있습니다.

Serial.print(timer0_millis);

이렇게 한번 찍어보세요. millis()함수와 같은 시간값을 출력됩니다. 내부적으로 타이머변수 값이 갱신이 되는데 이 변수를 호출할 때 호출한 시점의 시간값을 담고 있습니다. timer0_millis를 millis()함수 대신해서 사용할 수 있습니다. 그런데 추천은 드리지 않습니다. 외부전역변수 자체가 하드웨어 내부에 사용하는 변수들을 건들게 되면 정상적으로 하드웨어가 동작하지 못하는 경우가 발생하기 때문에 특별한 목적이 없는 한 사용하지 않는 방향으로 코딩 하셨으면 합니다.

마무리


이렇게 해서 시리얼모니터로 입력한 시간값을 기준으로 아두이노의 시계는 동작하게 됩니다. 이걸 시리얼모니터로 출력했지만 다른 외부 출력 부품을 이용하면 아두이노 전용 시계가 만들어 지겠죠. 참고로 7-Segment Dispaly로 출력하면 디지털 시계가 되는 것이고 Stepper Motor로 회전 시키면 아날로그 시계가 됩니다. LED로 시계판을 만들면 LED 시계가 되겠죠. 이건 어떤 출력부품을 사용하느냐에 따라서 다양한 아두이노 시계를 만들 수 있게 됩니다.

한번 오늘 배운 시계 원리를 이용하여 어떤 아두이노 시계를 만들지 상상의 나래를 펼쳐 보세요.


댓글()

[아두이노] 시간(시/분/초) 변환

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

[아두이노] 시간(시/분/초) 변환



오늘은 시간 변환을 실험을 하겠습니다. 초를 입력하여 시/분/초로 변환하는 실험과 문자열 시/분/초를 입력하여 시간, 분, 초로 분리해내는 실험을 하겟습니다 이 원리를 기반으로 시/분/초를 초로 변환시키 후 변환 시킨 초를 기준으로 시/분/초로 다시 원상 복구하여 시간 변환 코딩이 정상적으로 이루어지는지 실험 하겠습니다.


이제 본격적으로 실험을 해볼까요.

1. 초를 시/분/초로 변환



초를 시/분/초로 변환식 :

readTime = 입력Sec;

sec = 전체수 % 60;
min = (전체수/60)%60;
hour = (전체수/(60x60)%24;
day = (전체수/(60x60x24)%365;
...


1분 = 60초
1시간 = 60x60초
1day = 60x60x24초


시리얼모니터에서 초를 입력하면,

  if(Serial.available()){
    unsigned long readTime = Serial.parseInt();    
  }

아두이노에서 parseInt()로 간단히 읽는다. 총 15자리 수까지 입력받을 수 있다.

계산은,

    sec = readTime%60;
    min = (readTime/60)%60;
    hour = (readTime/(60*60))%24;       

시/분/초만 출력하게 하기 위해서는 위 코딩과 같이 표현하시면 됩니다. 참고로, 24시간 이상이면 day까지 구하면 좋지만 그냥 day은 무시하고 시계처럼 시/분/초만 출력합니다.

[소스]

int hour, min, sec;

void setup()
{
  Serial.begin(9600);  
}

void loop()
{
  //초 입력
  if(Serial.available()){
    unsigned long readTime = Serial.parseInt();    
   
    sec = readTime%60;
    min = (readTime/60)%60;
    hour = (readTime/(60*60))%24;       
      
    Serial.print(hour);
    Serial.print(" : ");
    Serial.print(min);
    Serial.print(" : ");
    Serial.println(sec);       
  }
}

[결과]


2. 시/분/초 문자열에서 시간 분리



입력 => "시:분:초"

위와같이 시/분/초로 입력 받는 다면 어떻게 표현 할까요. 위에 사전학습에서 String에 대해 사전학습을 미리 해 주세요.

시간 문자열을 분리해 내는 방법을 간단히 복습해 봅시다.

String inString = "10:11:22"

위 문자열을 "시:분:초"를 어떻게 분리해 낼까요. 시/분/초의 문자열을 나누는 기호가 ':'입니다. 즉, ':'의 위치를 알고 있으면 이 위치를 기준으로 시/분/초를 읽으면 됩니다.

int index1 = inString.indexOf(':'); 
int index2 = inString.indexOf(':',index1+1);   
int index3 = inString.length();
  • indexOf(':') : ':'의 기호가 있는 위치
  • index(':',검색시작위치) : 검색시작위치부터 ':'의 기호가 있는 위치

이렇게 index들을 구하면 즉, 문자열에서 수자를 읽을 마지막 위치값을 지정할 수 있습니다.

"10:11:22"
index1 = 2
index2 = 5
index3 = 8

index3이 7이 아니고 8인 이유는 문자열은 마지막 끝을 나타내는 문자기호가 포함되어 있기 때문에 index가 8입니다.

이제 위치값을 알았으니깐 시/분/초를 구해 볼까요.

hour = inString.substring(0, index1).toInt();
min = inString.substring(index1+1,index2).toInt();
sec = inString.substring(index2+1,index3).toInt();
  • substring(시작위치,마지막위치) : 특정부분문자열 추출
  • toInt() : 문자열을 정수형으로 변환

inString.substring(0, index1).toInt();

위 명령은 전체문자열에서 특정부분문자열을 추출하고 그 추출된 문자열을 정수형으로 변환시킨다는 명령입니다. hour, min, sec를 이 명령으로 시/분/초를 구할 수 있게 됩니다.

정확히 시/분/초가 분리되었는지 살펴 볼까요.

void setup()
{
  Serial.begin(9600);  
}

void loop()
{ 
  // 시:분:초 입력
  if(Serial.available()){
    String inString = Serial.readStringUntil('\n');    
    int index1 = inString.indexOf(':'); 
    int index2 = inString.indexOf(':',index1+1);   
    int index3 = inString.length();
    int hour = inString.substring(0, index1).toInt();
    int min = inString.substring(index1+1,index2).toInt();
    int sec = inString.substring(index2+1,index3).toInt();
        
    Serial.print(hour);        
    Serial.print(" : ");   
    Serial.print(min);
    Serial.print(" : ");   
    Serial.println(sec);     
  }
}

[결과]


위 그림처럼 시/분/초를 입력한 문자열이 제대로 분리되어 정확히 시/분/초가 출력 됩니다.

3. 시/분/초 입력값을 초 변환 후 다시 시/분/초 변환



이번에는 시/분/초를 입력 받은 후에 그 값을 초로 변환 시킵니다. 그리고 그 초 값을 다시 시/분/초로 변환하여 결과를 살펴보겠습니다. 시간 변환을 잘 이루어지는 살펴보기 위한 실험입니다.

시/분/초를 입력받은 문자열을 초로 변환하기 위해서는 다음과 같습니다.

totalSec = 시*3600 + 분*60 + 초;

시간 문자열을 읽어서 hour, min, sec 값을 분리 해내고 초로 변환하면 다음과 같이 코딩을 하면 되겠죠.

if(Serial.available()){
  String inString = Serial.readStringUntil('\n');    
  int index1 = inString.indexOf(':'); 
  int index2 = inString.indexOf(':',index1+1);   
  int index3 = inString.length();
    
  hour = inString.substring(0, index1).toInt();
  min = inString.substring(index1+1,index2).toInt();
  sec = inString.substring(index2+1,index3).toInt();
  
  readTime = (long)hour*3600+min*60+sec;
}   

위 코딩 식의 의해서 readTime 총 sec 값을 저장하게 됩니다.

다시, readTime 시/분/초 변환을 시켜서 계산 상 아무런 문제가 없는지 확인해 봅시다.

readTime = (long)hour*3600+min*60+sec;
sec = readTime%60;
min = (readTime/60)%60;
hour = (readTime/(60*60))%24;  

이렇게 다시 hour, min, sec 구하여 이 값이 시리얼모니터로 출력시키면 확인이 가능해 집니다.

[소스]

unsigned long readTime;
int hour, min, sec;

void setup()
{
  Serial.begin(9600);  
}

void loop()
{
  if(Serial.available()){
    String inString = Serial.readStringUntil('\n');    
    int index1 = inString.indexOf(':'); 
    int index2 = inString.indexOf(':',index1+1);   
    int index3 = inString.length();
    
    hour = inString.substring(0, index1).toInt();
    min = inString.substring(index1+1,index2).toInt();
    sec = inString.substring(index2+1,index3).toInt();
      
    readTime = (long)hour*3600+min*60+sec;
    
    Serial.print("Total Sec : ");
    Serial.println(readTime);   
   
    sec = readTime%60;
    min = (readTime/60)%60;
    hour = (readTime/(60*60))%24;  
      
    Serial.print(hour);
    Serial.print(" : ");
    Serial.print(min);
    Serial.print(" : ");
    Serial.println(sec);       
  }
}

[결과]


위 결과를 보면 "11:22:33" 시간이 입력되면 시간에 대한 초 변환 값 '40953'값이 나왔고 이 값을 다시 시/분/초로 변환하면 "11:22:33" 시간이 나옵니다. 시간 변환이 깔끔하게 되었네요.

마무리


오늘은 시간에 대한 코딩 트레이닝을 하였습니다. 아두이노에서 시간를 다룰 줄 알면 시간에 관한 다양한 표현을 할 수 있습니다. 오늘 배운 원리를 이해하시면 아두이노 시계를 만들 수 있습니다. 아두이노에 전원을 공급하고 현재 시간값을 입력하여 millis()함수를 이용하면 아두이노 시계를 만들 수 있습니다.

지난 시간에는 millis()함수로 시간값을 분리하고 카운트하는 방법을 살펴보았으며 오늘은 시간 변환을 살펴보았습니다. 한번 지끔까지 배운 원리를 종합하여 아두이노 시계를 만들어 보세요.

댓글()

[아두이노] 시간 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를 다루기 위해서 숫자를 쪼갤 수 있는 코딩과 타이머시간값에 대한 이해가 필요하기 때문에 좀 복잡한 코딩으로 설명을 하게 되었습니다.

댓글()

[아두이노] 시간 millis()함수로 읽은 숫자 쪼개기

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

[아두이노] 시간 millis()함수로 읽은 숫자 쪼개기



최근 내용들이 코딩 난이도를 살작 올려서 포스팅이 되고 있는데 오늘도 코딩에 대한 내용을 채워질 것 같습니다. 아두이노의 전원이 공급되면 타이머가 돌기 시작합니다. 타이머가 도는 시간을 가져오기 위해서 millis()함수를 사용합니다. 현재 타이머의 시간값을 읽어오게 됩니다. 1초당 1000의 값을 갖게 되는데 이 시간값을 통해 우리는 시간을 만들어 낼 수 있습니다. 여기서, millis()함수를 통해서 읽은 각 자리의 숫자들을 하나씩 쪼개는 실험을 하겠습니다.


1. millis()함수


unsigned long millisTime = millis();

이렇게 시간값을 최대사이즈로 담을 수 있는 변수를 선언합니다. 아두이노에 전원이 공급되면 그때부터 타이머가 동작하고 타이머 변수에 숫자가 증가하는데 그 값을 millis()함수가 읽어서 millisTime 변수에 저장하게 됩니다.

1초 = 1000

정확히 어떤 값이 찍히는지 아래 움짤로 살펴보도록 할까요.
[소스]

void setup()
{
  Serial.begin(9600);  
}
void loop()
{
  unsigned long millisTime = millis();
  Serial.println(millisTime);  
}

[결과]


가상시뮬레이터의 실행 시키면 타이머가 동작하여 millis()함수를 통해 읽은 시간값을 시리얼모니터로 출력되는 것을 보실 수 있을 꺼에요. 1초가 1000입니다.

2. millis()함수로 읽은 시간값 쪼개기


1) 수식

  unsigned long millisTime = millis();
    
  int v1 = millisTime%10; //일의 자리
  int v2 = (millisTime/10)%10; //10의 자리
  int v3 = (millisTime/100)%10; //100의 자리
  int v4 = (millisTime/1000)%10; //1000의 자리

C언어에선 몫과 나머지 구하는 코딩을 알아야 합니다.

몫 : 123/10 => 12
나머지 : 123%10 =>3

그러면, millisTime에 저장된 시간값의 일의 자리를 구할려면 millisTime값을 10으로 나눈 나머지 값이 1의 자리가 됩니다. 이 원리를 이용해서 다음 10의 자리를 구할려면 어떻게 할까요. 일의 자리가 필요 없기 때문에 1의 자리를 버릴려면 millisTime값을 10으로 나눈 몫의 값을 구하면 됩니다. 그러면, 10이하의 1의 자리가 버려 집니다. 그 몫에다 다시 10을 나눈 나머지 값을 구하면 10의 자리 숫자를 구할 수 있게 됩니다.

쉽게 수식으로 살펴보면,

123%10 => 3
(123/10)%10 => (12)%10 => 2
(123/100)%10 => (1)%10 => 1

이렇게 자릿수 값들을 구하게 됩니다. 한번 가상시뮬레이터로 돌려 볼까요.

[소스]

void setup()
{
  Serial.begin(9600);
}
void loop()
{
  unsigned long millisTime = millis();
  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);
}

[결과]


각 숫자의 자리 수값들을 구하는게 그렇게 어렵지 않죠.

참고로, 원래 초단위로 일의 자리를 출력되게 할려면 다음과 같이 해줘야 합니다.

millisTime = millis()/1000;

처음 millisTime에 millis() 값을 1000으로 나눈 몫으로 하면 millisTime변수에 1이 들어 있으면 그 숫자는 1초가 됩니다. 그런데 가상시뮬레이터로 실험하는데 일의 자리를 1초로 실행시키면 전체 결과를 보는데 한참 걸리겠죠. 그래서 1000으로 나누지 않고 그냥 직접 숫자를 쪼개서 출력했네요.

2) 또 다른 방법


  unsigned long millisTime = millis()/1000;
  int v1 = millisTime%10;
  int v2 = (millisTime/10)%10;
  int v3 = (millisTime/100)%10;
  int v4 = (millisTime/1000)%10;

위 코딩에서 millis()/1000으로 세팅하면 1초가 일의 자리로 해서 출발하게 됩니다. 이 방법과 유사하게 아래와 같은 방식으로도 표현이 가능합니다.

unsigned long timeVal=0; //이전 시간

void loop(){
  if(millis()-timeVal>=1000){
   d1++; 
   timeVal=millis();
  }
  if(d1==10){
    d1=0;
    d2++; 
  }
  if(d2==10){
    d2=0;
    d3++;    
  }
  if(d3==10){
    d3=0;
    d4++;    
  }
  if(d4==10){
    d4=0;    
  }
}

if문으로 현재시간(millis()) - 이전시간(timeVal)의 차이가 1000보다 크거나 같다면 1초를 의미하게 됩니다 .그 때 d1을 1씩 증가 시키게 됩니다 이 말은 1초씩 증가한다는 의미랑 같습니다. 그리고, 1초일때 마다 timeVal은 millis()함수의 값을 저장함으로써 다음 1초를 비교하기 위한 이전시간값이 저장됩니다.

이걸 움짤로 촬영할려고 했더니 가상시뮬레이터가 너무 느리게 반응해서 다음과 같이 수정했습니다.

if(millis()-timeVal>=10){
   d1++; 
   timeVal=millis();
}

d1 카운터를 10으로 낮추어서 돌도록 설정 했네요. 1초가 1000이니깐 0.01초 카운터가 되겠죠.

[결과]


3. 시간을 쪼개는 이유


7-segment display 와 같은 부품에 값을 출력할 때는 각 자리 숫자를 분해가 필요 합니다. 타이머 카운터 같은곳에서 응용으로 사용하기 딱 좋겠죠. 그리고, 이번에 post에서 다룬 이유는 다음에 4-digit 7-segment display를 사용한 실험을 하기 위해서 입니다. 1초 단위로 숫자를 4-digit 7-segment display에 출력을 한다면 millis()함수에서 읽은 시간값을 분해를 할 수 있는 능력이 필요하기 때문에 사전에 미리 학습차원으로 다루었네요.

마무리


오늘은 millis()함수에 대해서 간단히 살펴보았습니다. 알아두시면 나중에 아두이노를 다룰 때 유용하게 써먹을 수 있습니다.

댓글()

[아두이노] 온도센서를 이용한 경보 온도계 다른 접근

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

[아두이노] 온도센서를 이용한 경보 온도계 다른 접근 




[주제 ]

  • 일반스위치버턴을 인터럽트스위치버턴으로 변경
  • 50도 이상 증가 한 후 50이하로 떨어진 순간에 현재 온도가 출력되게 변경

지난시간에 간단히 경보 온도계를 만들었는데 그냥 넘어가기가 그래서 코딩적 접근 방법을 다르게 시도해보는 시간을 갖도록 하겠습니다. 하드웨어 부분은 동일하지만 어떻게 코딩을 하냐에 따라서 다양한 결과를 만들어 낼 수 있습니다.

1. 경보 온도계 회로도



경보 온도계는 지난 시간과 동일합니다. 참고에 지난시간의 post를 보고 사전학습을 미리 해주세요.


2. 코딩


[ 기본 소스 ]

#include <LiquidCrystal.h>

//LiquidCrystal(rs, enable, d4, d5, d6, d7) 
LiquidCrystal lcd(3, 4, 8, 9, 10, 11);

const int switchPin = 2;
const int piezoPin = 12;
unsigned long timeVal=0;
float V=0;
float C=0;
float F=0;

void setup() {
  pinMode(switchPin, INPUT_PULLUP);

  lcd.begin(16,2);
  lcd.setCursor(0, 0);
  lcd.print("Hello World");
  delay(1000);
  lcd.clear();
}

void loop() { 
  V =fmap(analogRead(A0),0,1023,0,5); //map함수 원리를 이용한 다이렉트 Voltage계산
  //float V = analogRead(A0)*5.0/1023;

  C = (V-0.5)*100;  //섭씨 C = (F-32)*1.8;
  F = C*9.0/5.0+32; //화씨 F = C*1.8+32;
    
  if(digitalRead(switchPin)== LOW){
    output();
    timeVal=millis();
    delay(500);
  }
  if(millis()-timeVal>=2000){
    output();  
    timeVal=millis();
  }
  if(C>50){
    output();
    tone(piezoPin,523,1000/8);     // 도음으로 8분음표(음길이)
    delay(1000/4*1.30);             
    noTone(12);
    timeVal=millis();
  }
}  
 void output(){
   lcd.clear();
   lcd.setCursor(0, 0);
   lcd.print("C : ");
   lcd.setCursor(6, 0);
   lcd.print(C);  
   lcd.setCursor(0, 1);
   lcd.print("F : ");
   lcd.setCursor(6, 1);
   lcd.print(F);  
  }

float fmap(long x, long in_min, long in_max, float out_min, float out_max)
{
  return (x - in_min) * (out_max - out_min) /(float) (in_max - in_min) + out_min;
}

지난시간의 실험한 기본 소스입니다. 이 소스에 대한 설명은 생략하겠습니다. 모르는분들은 지난시간의 post를 보고 오시기 바랍니다. 기본소스에 어떤 부분이 수정되고 추가 되었는지를 비교하면서 보시기 바랍니다.

1) 스위치 버턴을 인터럽트 스위치버턴으로 변경


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

void setup() { 
  pinMode(interruptPin, INPUT_PULLUP); 
  attachInterrupt(digitalPinToInterrupt(interruptPin), switchFn, FALLING);
}

void switchFn(){
   인터럽트 발생 시 처리 명령문;
}

아두이노우노는 2,3번이 인터럽트 핀입니다. 다른 핀을 사용 할 경우는 반응하지 않습니다. 다른 아두이노보드 일 경우는 해당 보드의 인터럽트 핀 넘버가 다를 수 있음으로 해당 보드의 인터럽트 핀 정보를 찾아보시기 바랍니다.

attachInterrupt(digitalPinToInterrupt(interruptPin), switchFn, FALLING);
FALLING : HIGH -> LOW로 변할때

이렇게 setup()함수 안에다 인터럽트 함수를 선언해 놓으면 해당 interruptPin에 전류가 HIGH->LOW로 변할 때 switchFn()함수를 호출하게 됩니다. 해당 인터럽트 2번핀은 풀업모드의 스위치버턴입니다. 처음 상태가 HIGH이고 스위치를 누르면 LOW이기 때문에 스위치를 누를대 HIGH->LOW로 변하게 됩니다. 그러면, switchFn()함수가 호출이 됩니다. 어떤 원리인지 아시겠지요.

[기존소스]

const int switchPin = 2;
void setup() {
  pinMode(switchPin, INPUT_PULLUP);
}
void loop(){
  if(digitalRead(switchPin)== LOW){
    output();
    timeVal=millis();
    delay(500);
  }
}   

변경 후,

boolean switchState=false; //스위치 상태값

void setup() { 
  pinMode(interruptPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(interruptPin), switchFn, FALLING);
}
void loop(){
  if(switchState==true){
    output();    
    timeVal=millis();    
    switchState=false;
  }
}   
void switchFn(){
  switchState=true;
}

같은 동작의 같은 느낌의 코딩입니다. 하지만 어떤 차이가 있을까요. 변경 전 스위치 버턴은 loop()함수에서 무한 반복할때 순서대로 명령문을 처리할 때 스위치가 눌러졌는지 확인하는 원리이고 변경 후 스위치 버턴은 loop()함수에서 무한 반복하는 명령문들의 순서랑 상관없이 스위치버턴 이벤트가 발생합니다. 즉, loop()함수와는 별개로 독립적으로 스위치버턴 동작을 수행합니다.

loop()함수의 지배권에 있느냐 독립적으로 있느냐에 차이인 것이죠. 사용목적에 따라서 사용하시면 됩니다.

2) 경보 해제 후 온도 출력


[기존소스]

void loop(){
  if(C>50){ 
    output();
    tone(piezoPin,523,1000/8);     // 도음으로 8분음표(음길이)
    delay(1000/4*1.30);             
    noTone(12);    
    timeVal=millis();    
    tmpState=true;    
  }  
}

지난시간에 숙제로 남겨 둔 50이상일 때 경보음과 현재 온도를 출력하게 코딩을 했었습니다. 그렇다면 50도 이상이 되었다가 50도 이하가 되는 순간 온도는 수정되어야 하지만 지난 소스에서는 바로 수정되는게 아니라 정해진 시간이 될 때까지는 온도값이 출력되지 않습니다. 그 부분을 해결하라고 숙제를 내줬는데 해결책은 알아 볼까요.

어떤 상태가 되면 그 상태가 되었을 때 그 상태에 대한 상태값을 변수로 저장하면 됩니다. 50이상이 된다면 이상이 되었을 때의 상태값을 가지고 있을 때 그 상태값을 기준으로 50이하가 된 순가 현재 온도를 출력시키면 해결 됩니다. 설명이 어렵게 되었는데 코딩을 살펴볼까요.

if(C>50){
 tmpState=true;
}
if(tmpState==true){
  if(C<50){
    tmpState=false;
  }
}

C의 온도가 50도 이상이면 tmpState은 true로 50이상인 상태가 되었다는 것을 상태변수에 저장해 놓습니다. 그러기 다음 if문을 통해서 50이상인 상태가 되었다면 다시 if문으로 C가 50이하로 떨어졌는지 체크하게 되고 50이하로 떨어졌다면 다시 tmpState의 상태값은 원상태로 돌아가서 50이상이 될때까지는 false가 됩니다. 이 원리는 자주 사용하는 원리이니깐 이참에 꼭 알아두시기 바랍니다.

if문은 어떤 A조건의 상태가 되면 그 A상태를 기억 할 수 있고 그 A상태를 기준으로 B조건에 들어 갈 수 있고 B라 상태를 만들 수 있습니다. 서로 다른 조건에 대해서 상태에 따라서 개입을 하고 싶을 때 if문을 활용 한다는 점을 꼭 기억해 두세요.

변경 후,

boolean tmpState = false; //온도 상태값

void loop(){
  if(C>50){ 
    output();
    tone(piezoPin,523,1000/8);     // 도음으로 8분음표(음길이)
    delay(1000/4*1.30);             
    noTone(12);    
    timeVal=millis();    
    tmpState=true;    
  }  
  
  if(tmpState==true){
    if(C<50){ 
      timeVal=millis();    
      tmpState=false;    
      output();
    }    
  }
}

3. 종합 소스


2초 단위로 LCD16x2에 온도와 습도가 출력됩니다. 여기서 스위치버턴을 누르면 인터럽트함수가 호출되고 switchState 값을 true로 변경하여 스위치가 눌러졌음을 나타냅니다 그리고 나서 if문에서 switchState가 true면 현재 온도와 습도를 출력하게 되게 코딩이 되어 있습니다. 스위치를 눌렀을 때 현재 온도와 습도를 출력하게 만든 것이죠. 다음으로, 온도가 50이상일 때 경보음과 현재 온도가 출력되는데 tmpState 변수에 true로 50이상인 상태를 나타냅니다. 그 다음 if문으로 온도가 50이하가 될때 현재 온도가 출력되게 함으로써 실시간으로 50이상으로 올라갔다가 50이하로 떨어졌을 때 현재 온도가 출력되게 함으로써 지난 시간의 코딩에 비해 경보 온도계를 매끄럽게 표현이 되었네요.

#include <LiquidCrystal.h>

//LiquidCrystal(rs, enable, d4, d5, d6, d7) 
LiquidCrystal lcd(3, 4, 8, 9, 10, 11);


//const int switchPin = 2;
const int interruptPin = 2;//인터럽트핀
boolean switchState=false; //스위치 상태값
boolean tmpState = false; //온도 상태값

const int piezoPin = 12;
unsigned long timeVal=0;
float V=0;
float C=0;
float F=0;


void setup() { 
  pinMode(interruptPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(interruptPin), switchFn, FALLING);
  
  lcd.begin(16,2);
  lcd.setCursor(0, 0);
  lcd.print("Hello World");
  delay(1000);
  lcd.clear();

}

void loop() {
 
  V =fmap(analogRead(A0),0,1023,0,5); //map함수 원리를 이용한 다이렉트 Voltage계산
  
  //float V = analogRead(A0)*5.0/1023;

  C = (V-0.5)*100;  //섭씨 C = (F-32)*1.8;
  F = C*9.0/5.0+32; //화씨 F = C*1.8+32;
  
  
  if(switchState==true){
    output();    
    timeVal=millis();    
    switchState=false;
  }
  if(millis()-timeVal>=2000){
    output();  
    timeVal=millis();
  }
  
  if(C>50){ 
    output();
    tone(piezoPin,523,1000/8);     // 도음으로 8분음표(음길이)
    delay(1000/4*1.30);             
    noTone(12);    
    timeVal=millis();    
    tmpState=true;    
  }  
  
  if(tmpState==true){
    if(C<50){ 
      timeVal=millis();    
      tmpState=false;    
      output();
    }    
  }
}  

void output(){
   lcd.clear();
   lcd.setCursor(0, 0);
   lcd.print("C : ");
   lcd.setCursor(6, 0);
   lcd.print(C);  
   lcd.setCursor(0, 1);
   lcd.print("F : ");
   lcd.setCursor(6, 1);
   lcd.print(F);  
}
void switchFn(){
  switchState=true;
}
float fmap(long x, long in_min, long in_max, float out_min, float out_max)
{
  return (x - in_min) * (out_max - out_min) /(float) (in_max - in_min) + out_min;
}


4. 결과


영상으로는 정확히 의미를 전달되지 않은 것 같습니다. 직접 공개회로도에 가셔서 TMP36 온도센서를 가상시뮬레이터에서 직접 제어해서 결과를 직접 체험해 보셨으면 합니다.


마무리


오늘은 코딩에 대해서 살펴 보았습니다. 지난시간의 소스는 약간 불안정한 소스였고 오늘은 좀 더 개선된 소스 이면서 새롭게 인터럽트 방식을 적용해 보았습니다. 코딩은 딱 하나의 정답은 없습니다. 지난시간의 소스와 오늘 소스를 통해서 다양한 접근 방식의 코딩을 하셨으면 합니다. 어떤 코딩을 하고 그 코딩으로 결과가 나왔으니깐 마무리하지 말고 이런식으로 또 다른 접근법이 없는지 계속 상상을 하면 보이지 않았던 것이 보이고 코딩이 개선되고 상상코딩의 능력이 올라가게 됩니다.

그리고, 여러분들은 지금까지의 배웠던 부품 중 떠오르시는 것이 있으면 부품을 더 결합해서 재밌는 형태로 변형 시켜 보셨으면 합니다.


댓글()

[아두이노] 온도센서(TMP36)+LCD16x2 경보 온도계 응용

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

[아두이노] 온도센서(TMP36)+LCD16x2 경보 온도계 응용



오늘은 @uuu95님이 질문한 DHT11 실험을 가상시뮬레이터에서 한번 구현해보고 싶어서 실험을 하게 되었네요. DHT11 부품이 가상시뮬레이터에서 제공되지 않기에 TMP36 부품으로 대신하여 실험을 할까 합니다. 내용을 보니깐 온도를 읽어서 LCD16x2 모니터에 결과를 출력하는데 5초단위로 지속적으로 출력하고 스위치를 누르면 현재 온도를 출력하고 일정온도 이하가 되면 피에조부저가 경고음을 울리게 하는 실험이더군요. 한번 위 내용을 가상시뮬레이터에서 제 나름대로 재구성하여 실험을 하겠습니다.

참고로 오늘 실험을 하기전에 위에 링크가 걸린 참고 post들은 각 부품에 대한 기초 내용으로 구성되어 있기 때문에 가셔서 각 부품의 사용법을 사전학습하시고 오세요. 아시는 분들은 그냥 넘어가셔도 되고요 중복되는 내용은 구지 처음부터 설명을 할 필요는 없겠죠.


1. 온도계 회로도


  • 준비물 : 스위치버턴 1개, TMP36 1개, 아두이노우노
  • 내용 : 스위치버턴은 2번핀에 연결하고 TMP36은 A0핀에 연결하여 값을 읽는다.
  • 공개회로도 : https://www.tinkercad.com/things/k9ksK4Tk94a(TMP36+스위치버턴)


온도센서와 스위치 버턴을 사용하여 온도값을 시리얼 모니터로 출력시켜 코딩을 우선 합시다.

2. 코딩


코딩은 5초 단위로 TMP35 센서의 값을 읽어와 온도와 습도를 출력하도록 할려고 했는데 시뮬레이터를 빠르게 동작시키기 위해 2초 단위로 수정하여 온도와 습도가 나오게 할 예정입니다. 이때 스위치 버턴을 누르면 현재 온도와 습도가 출력되게 설계합니다. 그런데 2초 단위로 온도와 습도가 출력되는데 delay(2000) 함수를 사용하면 2초 동안은 스위치를 눌러도 반응을 하지 않습니다. 대기 시간에는 아두이노는 휴먼상태가 되기 때문에 스위치를 누를 수 없기 때문에 delay()함수를 사용하지 않는 delay 효과를 부여해서 제어함으로써 실시간으로 스위치가 눌러지면 현재 온도와 습도가 출력되고 2초 단위로 현재 온도와 습도가 출력되게 코딩을 만들어 봅시다.

[delay()함수 없이 delay] 2초 단위로 if문 안에 명령문 수행

unsigned long timeVal=0; //이전시간

void loop(){
  if(millis()-timeVal>=2000){ //딜레이 시간 2초(2000)
    2초 단위로 동작;
    timeVal=millis();
  }
}

식 : 현재시간(millis) - 이전시간(timeVal) > 딜레이시간(2000)

와 같이 코딩을 하면 2초 단위로 if문안의 명령문을 수행합니다. 수행하고 수행 후 timeVal은 수행된 시간 millis()시간을 저장함으로써 다음 2초 시간을 구할 때 사용합니다.

[스위치버턴 풀업모드버턴]

const int switchPin = 2;
void setup() { 
  pinMode(switchPin, INPUT_PULLUP);
}
void loop(){
  if(digitalRead(switchPin)== LOW){
    스위치가 눌러질때 명령문;
    delay(500);
  }
}

풀엄모드 스위치버턴이라서 pinMode()은 INPUT_PULLUP으로 선언됩니다. 동작은 스위치를 누르지 않는 상태는 HIGH상태이고 스위치를 누르면 LOW상태가 됩니다 그래서, loop()함수에서 스위치가 눌러졌을 때 LOW랑 같은지로 체크하게 됩니다.

[온/습도 계산]

float V=0;
float C=0;
float F=0;

void loop(){
  V =fmap(analogRead(A0),0,1023,0,5); //map함수 원리를 이용한 다이렉트 Voltage계산
  //V = analogRead(A0)*5.0/1023;
  C = (V-0.5)*100;  //섭씨 C = (F-32)*1.8;
  F = C*9.0/5.0+32; //화씨 F = C*1.8+32;
}

float fmap(long x, long in_min, long in_max, float out_min, float out_max)
{
  return (x - in_min) * (out_max - out_min) /(float) (in_max - in_min) + out_min;
}

C, F 공식에 대입하기 위해서 V값을 구할때 0~5V 사이의 전류값을 알아야 합니다. 하지만, analogRead(A0)함수로 읽게 되면은 0~1023의 값으로 읽게 되는데 그러면 주석으로 처리한 식으로 계산을 한번 더 해야 합니다. 그래서, map()함수를 변형 시켜서 다이렉트로 0~5V 사이의 전류값을 구하도록 수정했습니다. 원래 map()함수는 float형이 아닌데 실수값을 구해야 해서 float형으로 변형 사용자정의함수로 직접 만들었습니다 전류를 구해서 C, F를 식에 대입하여 온도와 습도를 구하게 됩니다.

설계

  1. TMP36을 읽어 온/습도를 계산한다.
  2. 스위치가 눌러졌는지 확인(스위치가 눌러졌으면 현재 온/습도 출력한다.) 출력된 시점을 기준으로 2초후 온습도가 출력하게 만든다.
  3. 2초단위로 온/습도를 출력한다.
const int switchPin = 2;
unsigned long timeVal=0;
float V=0;
float C=0;
float F=0;

void setup() {
  Serial.begin(9600);
  pinMode(switchPin, INPUT_PULLUP);
}

void loop() {

  //TMP36읽어 온도와 습도 계산
  V =fmap(analogRead(A0),0,1023,0,5); //map함수 원리를 이용한 다이렉트 Voltage계산
  //V = analogRead(A0)*5.0/1023;

  C = (V-0.5)*100;  //섭씨 C = (F-32)*1.8;
  F = C*9.0/5.0+32; //화씨 F = C*1.8+32;
  
  
  if(digitalRead(switchPin)== LOW){ //스위치 눌러졌는지
    output();
    timeVal=millis(); //현재시간을 이전시간변수에 저장
    delay(500);
  }
  if(millis()-timeVal>=2000){ //현재시간-이전시간의 차이 2000인지 확인
    output();  
    timeVal=millis(); //현재시간을 이전시간변수에 저장
  }  
}  

//온도와 습도 출력
void output(){   
   Serial.print("C : ");
   Serial.println(C);  
   Serial.print("F : ");   
   Serial.println(F);  
}

// TMP36에서 읽은 0~1023값을 0~5V로 변환
float fmap(long x, long in_min, long in_max, float out_min, float out_max)
{
  return (x - in_min) * (out_max - out_min) /(float) (in_max - in_min) + out_min;
}

딜레이 함수를 사용하지 않기 때문에 실시간으로 계속 온도가 측정이 되고 스위치가 눌러지면 그때 현재 온/습도 값이 시리얼모니터로 출력된다. 그리고 출력된 시점이 이전시간이 되어 다음 2초 후에 온/습도가 출력된다. 스위치가 안눌러졌다면 2초단위로 온/습도가 출력된다.

3. 결과


2초후 부터 지속적으로 온도와 습도가 출력되는데 스위치 버턴을 누르면 바로 온도와 습도가 출력됩니다. 출력시간은 여러분들이 2000 대신에 다른 값으로 수정하시면 원하는 시간대 출력되도록 만들 수 있습니다.


4. 온도계에 피에조부저로 경보 알림


1) 회로도


  • 준비물 : 피에조 부저 1개, 스위치버턴 1개, TMP36 1개, 아두이노우노
  • 내용 : 추가로 피에조 부저를 12번 핀에 연결한다.
  • 공개회로도 : https://www.tinkercad.com/things/20FHbzZpihk(TMP36+스위치버턴+피에조부저)


12번 핀을 통해 피에조부저에 경보음을 출력한다.

2) 코딩


if(온도>50){
    tone(12,523,1000/8);     // 도음으로 8분음표(음길이)
    delay(1000/4*1.30);             
    noTone(12);            
}

온도가 50도 이상이면 경보음이 울리게 됩니다. tone()~noTone()으로 도음을 끊어서 경보음이 울리게 한다.

[수정소스] 추가된 코드영역만 표시 합니다.

... 생략
const int piezoPin = 12;

void setup(){
  ... 생략
}
void loop() {
 
  ... 생략
    
    //온도가 50도이상이면 경보음
  if(C>50){
    output();
    tone(piezoPin,523,1000/8);     // 도음으로 8분음표(음길이)
    delay(1000/4*1.30);             
    noTone(12);
    timeVal=millis();
  }
}
... 생략

나머지 부분은 위의 소스랑 동일합니다. 온도가 50도 이상이면 해당 온도/습도를 시리얼모니터에 출력하고 경보움이 짧게 끊어서 울린다. 그리고 현재시간을 이전시간으로 저장한다. 용도는 온도가 50도 이하로 떨어질때까지 지속적으로 경보음이 울리게 하는 목적이라고 생각하시면 되겠습니다.

3)결과


움짤로 만들다 보니깐 소리가 안나네요. 참고로 온도가 50도 이상이 되면 연속으로 온도와 습도가 출력되는 모습을 보실꺼에요. 그때 피에조부저도 같이 소리가 나온다고 생각하시면 될 듯 싶네요. 피에조부저가 울리는 명령문 안에 온도와 습도가 같이 출력되도록 하는 명령이 들어 있으니깐요. 그걸 감안하시고 연속으로 온도와 습도가 나오는 이미지 위치가 피에조부저가 같이 경고음을 울린다고 상상하시면 되겠습니다. 단점은 50이상인 경우는 연속적으로 온도가 출력되지만 50 이상이였다가 50이하로 떨어지면 2초후에나 결과를 확인할 수 있습니다. 스위치 버턴을 눌러서 바로 결과를 확인할 수 있지만 2초간의 딜레이 시간이 주어지기 때문에 이부분은 또다른 if문 락을 걸어서 50이상였다가 이하로 떨어지는 순간에 if문 락이 걸린 문장이 수행되어 현재온도를 출력되게 수정하면 됩니다. 이부분은 코딩하지 않았습니다. 숙제로 한번 이문제를 해결해 보세요.


5. 온도계에 LCD16x2 연결


1) 회로도



//LiquidCrystal(rs, enable, d4, d5, d6, d7)
LiquidCrystal lcd(3, 4, 8, 9, 10, 11);

핀이름을 잘보시고 해당 핀을 연결하시면 됩니다. 어려우시다면 위 링크된 사전학습 자료에 가셔서 사전학습을 하고 오세요.


선 연결이 좀 복잡하실꺼에요. 그래서 위에서 부터 나눠서 회로도를 순차적으로 만들었네요.

3) 코딩


LCD16x2 모니터 출력하기 위해서는

#include <LiquidCrystal.h>

//LiquidCrystal(rs, enable, d4, d5, d6, d7) 
LiquidCrystal lcd(3, 4, 8, 9, 10, 11);

데이터 4개의 핀으로 해서 글자를 출력합니다.

void setup(){
  lcd.begin(16,2); //가로x세로
  lcd.setCursor(0, 0); //시작위치 첫번째줄의 0번째칸 위치에서 시작
  lcd.print("Hello World"); //시작위치에서 글자를 순서대로 출력
  delay(1000); 
  lcd.clear(); //출력된 값을 지움
}   

위 주석을 잘 확인하시면 됩니다.

 void output(){
   lcd.clear(); //LCD16x2에 값을 지우기
   lcd.setCursor(0, 0); //첫번째줄의 0번째 칸 위치
   lcd.print("C : "); //문자열 출력
   lcd.setCursor(6, 0); // 첫번째 줄 6번째 칸 위치
   lcd.print(C);  //C(온도값) 출력
   lcd.setCursor(0, 1); //두번째줄의 0번재 칸 위치
   lcd.print("F : "); //문자열 출력
   lcd.setCursor(6, 1); //두번째줄의 6번째 칸 위치
   lcd.print(F);  
 }

사용자 정의 output() 함수는 위에서는 Serial 모니터에 출력했다면 이제는 LCD16x2에 출력되어야 하기 때문에 Serial 대신에 lcd로만 변경해주시면 됩니다 lcd은 젤 처음 LCD16x2 객체변수로 선언한 네임입니다. 주석으로 다 달아놓았으니깐 간단히 읽어보시면 됩니다.

lcd.setCursor(칸, 줄) : 줄, 칸의 위치를 가리킴
lcd.print(값) : 줄, 칸의 위치에 값을 출력

이 두개의 함수만 알면 쉽게 LCD16x2에 값을 출력할 수 있습니다.

4) 종합소스


#include <LiquidCrystal.h>

//LiquidCrystal(rs, enable, d4, d5, d6, d7) 
LiquidCrystal lcd(3, 4, 8, 9, 10, 11);

const int switchPin = 2;
const int piezoPin = 12;
unsigned long timeVal=0;
float V=0;
float C=0;
float F=0;

void setup() {
  pinMode(switchPin, INPUT_PULLUP);

  lcd.begin(16,2);
  lcd.setCursor(0, 0);
  lcd.print("Hello World");
  delay(1000);
  lcd.clear();
}

void loop() { 
  V =fmap(analogRead(A0),0,1023,0,5); //map함수 원리를 이용한 다이렉트 Voltage계산
  //float V = analogRead(A0)*5.0/1023;

  C = (V-0.5)*100;  //섭씨 C = (F-32)*1.8;
  F = C*9.0/5.0+32; //화씨 F = C*1.8+32;
    
  if(digitalRead(switchPin)== LOW){
    output();
    timeVal=millis();
    delay(500);
  }
  if(millis()-timeVal>=2000){
    output();  
    timeVal=millis();
  }
  if(C>50){
    output();
    tone(piezoPin,523,1000/8);     // 도음으로 8분음표(음길이)
    delay(1000/4*1.30);             
    noTone(12);
    timeVal=millis();
  }
}  
 void output(){
   lcd.clear();
   lcd.setCursor(0, 0);
   lcd.print("C : ");
   lcd.setCursor(6, 0);
   lcd.print(C);  
   lcd.setCursor(0, 1);
   lcd.print("F : ");
   lcd.setCursor(6, 1);
   lcd.print(F);  
  }

float fmap(long x, long in_min, long in_max, float out_min, float out_max)
{
  return (x - in_min) * (out_max - out_min) /(float) (in_max - in_min) + out_min;
}

5) 결과


이 결과도 움짤로 하려다가 소리도 들리는 결과물이 있어야 할 것 같아서 그냥 동영상으로 결과물을 만들었네요.


마무리


세 파트로 나눠서 설명을 했네요. 한번에 전부 다 합쳐진 상태로 설명을 하면 좀 코딩을 어려워 할 수 있고 회로도도 너무 복잡해 보일 수 있기 때문에 나눠서 설명했습니다. 결과적으로 post가 좀 길어졌네요. 설명도 사전학습내용은 다 뺄려고 했는데 빼면은 설명이 안되는 부분이 있어서 어쩔 수 없이 복습차원으로 추가했네요.

아무튼 @uuu95님이 실험하시는 주제에 대해 가상시뮬레이터로 재미있게 실험을 하셨습니다. 참고로 세파트로 나누어진 회로도는 공개회로도에 다 있으니깐 가셔서 한번 가상시뮬레이터를 직접 돌려보시고 어떻게 동작하는지 살펴보셨으면 합니다.


댓글()

[아두이노] 수위 센서(Water Level Sensor) 제어

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

[아두이노] 수위 센서(Water Level Sensor) 제어



오늘은 수위 센서라는 물의 높이를 측정할 수 있는 부품 사용법에 대해서 간단히 살퍄보도록 하겠습니다. 수위 센서 표면에 물이 닿는 위치에 따라서 수위 센서의 값이 0~1023사이의 값을 발생합니다. 그 값을 통해서 물의 높이를 알 수 있는 부품입니다. 실험에 사용한 수위 센서는 정교한 측정 도구는 아니고 간단히 물의 높이를 측정하는 실험용 모듈로 생각하시면 될 듯 싶습니다.

본격적으로 Water Level Sensor에 대해 알아보도록 할까요.

1. Water Level Sensor




위 센서는 세개의 핀이 있는데 보시는 것처럼 (-,+,S)로 표시되어 있는데 해당핀들은 (Gnd, Vcc, Analog Signal)로 구성되어 있습니다. 수위 센서의 표면에 긴선 모양을 보이실 꺼에요 이 센서의 전류가 공급되면 물이 이 센서의 표현에 닿게 되면은 전류의 값이 변화게 됩니다. 물은 전류가 흐르기 때문에 이 선들이 물에 접촉하게되면 전류의 값이 증가하게 됩니다. 물의 접촉에 따라서 늘어날수록 전류의 값은 커지겠지요. 이때 Analog Signal Pin을 통해서 그 전류의 값이 읽게 됩니다. 아두이노에서는 아날로그 신호를 읽을 때 0~1024사이의 값을 읽게 됩니다.

아날로그 읽기 : analogRead(아날로그핀)

2. Water Level Sensor 회로도


  • 준비물 : Water Level Sensor 1개, 아두이노우노
  • 내용 : A0에 Analog Signal Pin을 연결한다.


선 연결은 위 그림처럼 단순합니다.

3. 코딩


  • analogRead(A0) : A0핀에서 아날로그 신호(0~1024) 읽음

analogRead(A0) 함수로 아날로그 읽기함수로 수위 센서의 전류 0~1023사이의 값을 읽게 됩니다. 전류 신호값이 크면 클수록 물의 수위가 높아집니다.

void setup(){
  Serial.begin(9600);
}
void loop(){
  int waterSensorVal = analogRead(A0); 
  Serial.print("Water Sensor : ");
  Serial.println(waterSensorVal);
  delay(100);  
}

4. 결과


post를 등록할려고 보니 영상물에 결과에서 글자가 오타가 났네요. water를 warter라고 오타 났네요.(ㅋㅋ)

Water Level Sensor 문제점


수위 센서는 사실 접촉식은 별로 추천하지 않습니다. 그 이유는 물을 직접적으로 접촉하기 때문에 장시간 물에 접촉된다면 부식과 이물질이 붙는 문제도 있고 물기가 남아있으면 정상적인 결과가 나오지 않습니다. 즉, 수위가 낮아져도 물기가 수위 센서에 뭍어있기 때문에 그 물기가 수위 값으로 판정해 버린다는 문제점이 있습니다. 영상을 보시면 컵에서 꺼낸 수위 센서에서 계속 수위 값을 출력되고 화장지로 닦으니깐 수위 센서값이 0으로 되돌아가는 것을 영상에서 보실 수 있을 꺼에요. 그래서, 뭔가를 만들고 싶다면 접촉식보다 비접촉식으로 수위를 측정하는 센서를 사용하는게 좋습니다. 비접촉식 수위 센서는 구글검색으로 통해서 한번 찾아보세요.

마무리


오늘이 간단히 이런 종류의 센서가 있다는 것을 알려드리려고 post에 담았습니다. 수위를 측정하는 방법이 단순하죠. 수위 센서로 여러분들은 어떤 것을 하고 싶으신지 한번 상상을 해보세요. 일상에서 물의 높이를 측정하는 곳은 물탱크가 있는 곳에서 일반적으로 사용합니다. 물탱크의 물의 수위를 측정하여 물탱크의 물을 채울 수 있습니다. 즉, 물의 높이를 측정하면 물의 용량을 조절할 수 있게 됩니다. 물이 있는 곳에서는 수위센서가 요긴하게 사용됩니다. 수위조절에 필요한 물의 대한 기본 데이터를 제공하기 때문이지요. 수위를 측정하는 예를 들면, 댐이나 강에서 물의 높을 측정할 수 있다면 수문 제어를 할 수 있습니다. 수문을 언제 개방하도 닫을 것인지를요. 수경재배의 물탱크에도 식물에 물 공급과 양액 공급에 조절을 할 수 있겠죠.

수위 센서를 어디에 쓸지 상상을 해보세요. 제가 이 수위 센서를 사용한다면 물기에 대해 센서의 값의 변화가 일어나기 때문에 물기 감지센서로도 사용하면 것은 어떨까 하고 상상하게 되네요. 즉, 물기가 없으면 수위센서의 값은 0이 됩니다. 하지만 물기가 수위센서에 닿게 되면은 닿은 면적에 대한 전류값이 발생하고 수위센서의 물기에 대한 일정 전기신호가 만들어 집니다. 물기 감지센서로 사용하면 좋겠죠.

여러분들도 물에 대해 측정할 수 있는 이 수위 센서를 가지고 어떤 것을 만들 지 한번 상상의 나래를 펼쳐 보세요.


댓글()

[아두이노] 지그재그 주행 패턴 아두이노 RC카 응용

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

[아두이노] 지그재그 주행 패턴 아두이노 RC카 응용 



지난 시간까지 해서 아두이노 기본 키보드/마우스 동작 명령을 실험을 해보았습니다. 오늘은 아두이노 IDE의 USB 예제에서 "KeyboardReprogram" 소스가 있는데 매크로 키보드 제어 예제로 딱 좋을 것 같아서 소개를 하도록 하겠습니다. 아두이노를 이용하여 PC에서 수행 할 명령을 기록해 놓았다가 기록 된 순서대로 명령을 내릴 수 있습니다. 이 원리를 이용하면 상상력에 따라 다양한 오토 매크로 프로그램을 만들 수 있습니다.

1. 지그재그 주행 패턴 동작 원리


아두이노 RC카가 위 그림처럼 주행 패턴을 만든다면 일종의 로봇청소기와 같은 주행을 만들 수 있겠죠. 이 주행은 과연 어떻게 이뤄질까요.

첫줄은 과연 어떻게 주행 할까요. 위 그림에서 한 칸당 1초 주행이라면 총 6초(전체이동시간)동안 주행하면 첫줄 주행이 끝나겠죠.

motor1.run(FORWARD);
motor2.run(FORWARD);
delay(전체이동시간);

첫줄에서 두번째 줄로 넘어갈려면 90도 회전 후 0.2초 이동 후 다시 같은 방향으로 90도 회전합니다.

위 그림과 같은 회전 모습을 보여야 겠죠.


motor1.run(FORWARD);
motor2.run(BACKWARD);
delay(90도우회전시간);
motor1.run(FORWARD);
motor2.run(FORWARD);
delay(다음라인이동시간);
motor1.run(FORWARD);
motor2.run(BACKWARD);
delay(90도우회전시간);


두번째 줄에서 세번째 줄로 넘어갈려면 위 코딩에서 반대 방향으로 90도 회전 후 0.2초 이동 후 다시 같은 방향으로 90도 회전합니다.

motor1.run(BACKWARD);
motor2.run(FORWARD);
delay(90도좌회전시간);
motor1.run(FORWARD);
motor2.run(FORWARD);
delay(다음라인이동시간);
motor1.run(BACKWARD);
motor2.run(FORWARD);
delay(90도좌회전시간);

종합해 보면,

//홀수라인 정주행
motor1.run(FORWARD);
motor2.run(FORWARD);
delay(전체이동시간);

//짝수다음라인으로 이동
motor1.run(FORWARD);
motor2.run(BACKWARD);
delay(90도우회전시간);
motor1.run(FORWARD);
motor2.run(FORWARD);
delay(다음라인이동시간);
motor1.run(FORWARD);
motor2.run(BACKWARD);
delay(90도우회전시간);

//짝수라인 역주행
motor1.run(FORWARD);
motor2.run(FORWARD);
delay(전체이동시간);

//홀수라인으로 이동
motor1.run(BACKWARD);
motor2.run(FORWARD);
delay(90도좌회전시간);
motor1.run(FORWARD);
motor2.run(FORWARD);
delay(다음라인이동시간);
motor1.run(BACKWARD);
motor2.run(FORWARD);
delay(90도좌회전시간);

이렇게 해서, 주행 하면 되겠죠.

2. 지그재그 주행 패턴 만들기

id movePattern(){
     state=!state; //방향전환상태값

     if(state==true){
       motor1.run(FORWARD);
       motor2.run(BACKWARD);
       delay(90도우회전시간);
       motor1.run(FORWARD);
       motor2.run(FORWARD);
       delay(다음라인이동시간);
       motor1.run(FORWARD);
       motor2.run(BACKWARD);
       delay(90도우회전시간);
     }
     else{
       motor1.run(BACKWARD);
       motor2.run(FORWARD);
       delay(90도좌회전시간);
       motor1.run(FORWARD);
       motor2.run(FORWARD);
       delay(다음라인이동시간);
       motor1.run(BACKWARD);
       motor2.run(FORWARD);
       delay(90도좌회전시간);
     }            
     motor1.run(FORWARD);
     motor2.run(FORWARD);
}

위 코딩을 보면 if문으로

state=!state; //방향전환상태값
if(state==true){
  짝수라인으로 이동;
}
else{
  홀수라인으로 이동;
}
전진: 

state 값을 다음 라인으로 이동할 때 마다 반전시켜서 홀수/짝수 라인을 교대로 방향이 바뀌게 if문으로 제어할 수 있겠죠.

2. 지그재그 주행


위 그림처럼 주행을 하게 하려면 어떻게 코딩해야 할까요.

아래 그림과 같이 상황이 주어질 경우를 살펴보겠습니다. 정해진 구간이 1~6칸까지로 주행을 하게 된다고 가정해 봅시다.

1에서 5까지 갔다가 다음라인에서 3까지 갔다가 다시 다음라인에서 6까지 이동해야 합니다. 이 주행을 하기 위해서 각 진행방향에 대한 남은 주행 거리를 계산해 내야 합니다. 주행을 계산하는 방법이 떠오른 것이 대충 3가지 종류가 되는데 일일히 다 설명하자니 너무 post가 길어질 것 같아서 제일 맘에 드는 방법 중 하나를 소개할까 합니다.

장애물 발견시 다음 라인 주행을 위한 시간을 구하는 식 :

[남은 주행거리 시간]
timetemp = 전체주행구간 - (현재시간-이전시간);
timetemp = maxTime - (millis() - timeVal);

[주행 시작위치시간 가정]
timeVal = 현재시간-주행거리 시간;

timeVal = millis()-timetemp;


주행 시작위치시간은 millis()함수로 현재 시간값을 기준으로 이전 남은주행거리 시간값을 빼주게 되면 이 남은주행거리 시간이 주행을 한걸로 간주하는 시간값으로 만들 수 있습니다. 설명이 좀 그런데 쉽게 숫자로 설명드리면 다음과 같습니다.

6칸을 6초로 6000이라고 하면 장애물 위치 millis()값이 5000이 될때 방향전환을 통해 짝수라인으로 이동하겠죠. 남은 주행거리 시간은 1000입니다. 이 1000 값을 millis()함수로 현재시간에서 1000을 빼주면 1000 동안 주행 한걸로 간주할 수 있게 됩니다. 그러면 1000만큼 주행한거니 남은 주행거리 시간은 5000이 됩니다. 어떤 의미인지 아시겠지요.

if(장애물감지){
  int timetemp = maxTime - (millis() - timeVal);
    주행방향전환패턴;
  timeVal = millis()-timetemp;
}

이렇게 해서 timeVal값은 이전시간변수이지만 위 식에 의해서 이전 남은 주행거리 시간값을 빼줌으로써 현재라인에서는 그 빼준 시간값만큼 주행한 걸로 간주하게 됩니다.

4. 종합 코딩


maxTime =>주행 구간거리
timeVal  => 이전시간

if(장애물감지){
  int timetemp = maxTime - (millis() - timeVal);
    주행방향전환패턴;
  timeVal = millis()-timetemp;
}
if(millis() - timeVal >= maxTime){   
    주행방햔전환패턴;
    timeVal = millis();
}

대충 위와 같은 과정을 거치게 됩니다. 전방에 장애물을 감지해야 하기 때문에 초음파센서로 장애물 감지하게 되면 다음과 같이 코딩을 완성할 수 있게 됩니다.

void loop() {
  delay(50);
  int distance = sonar.ping_cm();

  //장애물 감지
  if(distance>0 && distance<10){
     int timetemp = maxTime - (millis() - timeVal);
     movePattern();
     timeVal = millis()-timetemp;
  }
  else if(millis() - timeVal > maxTime){
    movePattern();
    timeVal = millis();
  }
}

전체소스를 종합해보면,

#include <AFMotor.h>
#include <NewPing.h>

AF_DCMotor motor1(3);
AF_DCMotor motor2(4);

const int TRIG = A4;
const int ECHO = A5;
const int MAX_DISTANCE = 100;
NewPing sonar(TRIG, ECHO, MAX_DISTANCE);

int speed = 200;
int maxTime = 5000; //주행거리
boolean state = false; //주행방향전환
unsigned long timeVal = 0; //이전시간값

void setup() {
//  Serial.begin(9600);
  motor1.setSpeed(speed);
  motor2.setSpeed(speed);
  motor1.run(RELEASE);
  motor2.run(RELEASE);
  delay(maxTime);

  //Start
  motor1.run(FORWARD);
  motor2.run(FORWARD);
}

void loop() {
  delay(50);
  int distance = sonar.ping_cm();

  //장애물 감지
  if(distance>0 && distance<10){
     int timetemp = maxTime - (millis() - timeVal);
     movePattern();
     timeVal = millis()-timetemp;
  }
  else if(millis() - timeVal > maxTime){
    movePattern();
    timeVal = millis();
  }
}

void movePattern(){
     state=!state; //방향전환상태값

     if(state==true){
       motor1.run(FORWARD);
       motor2.run(BACKWARD);
       delay(90도우회전시간);
       motor1.run(RELEASE);
       motor2.run(RELEASE);
       delay(100);
       motor1.run(FORWARD);
       motor2.run(FORWARD);
       delay(다음라인이동시간);
       motor1.run(RELEASE);
       motor2.run(RELEASE);
       delay(100);
       motor1.run(FORWARD);
       motor2.run(BACKWARD);
       delay(90도우회전시간);
     }
     else{
       motor1.run(BACKWARD);
       motor2.run(FORWARD);
       delay(90도좌회전시간);
       motor1.run(RELEASE);
       motor2.run(RELEASE);
       delay(100);
       motor1.run(FORWARD);
       motor2.run(FORWARD);
       delay(다음라인이동시간);
       motor1.run(RELEASE);
       motor2.run(RELEASE);
       delay(100);
       motor1.run(BACKWARD);
       motor2.run(FORWARD);
       delay(90도좌회전시간);
     }            
     motor1.run(RELEASE);
     motor2.run(RELEASE);
     delay(100);
     motor1.run(FORWARD);
     motor2.run(FORWARD);
} 

위 소스는 최근 아두이노 RC카 소스를 기반으로 오늘 코딩만 삽입하여 수정한 소스입니다. 코딩상으로는 이렇게 설계할 수 있습니다. 하지만 안타깝게는 정상적인 주행 결과를 얻을 수 없습니다. post에서 실패한 사례입니다. 즉흥적으로 상상하고 그걸 바로 코딩화하고 바로 post에 옮기니깐 실패할때는 난감해지네요. 계속 회전각과 일직선주행에 대하 반복 실험을 해서 정확한 각도 시간과 일직선문제를 해결해야 하는데 시간적 여건이 안되어서 이건 그냥 포기할 까 합니다. 여러분들이 시간이 남는 분이 있으시면 이 소스를 기반으로 한번 시간값을 반복 실험을 통해서 각도와 일직선주행 문제를 해결해 보세요. 

우선 단순하게 생각해서 좌/우회전각도시간값을 500으로 하고 다음라인이동시간을 500정도로 잡고 주행을 시도했는데 회전각도와 양쪽 DC기어모터의 순간 속도 차이로 인해 일직선 주행과 90도 회전이 되지 않았습니다. 근사각도로 변경은 되었지만 사실상 오차각도만큼의 주행라인이 크게 변화되는 현상이 발생했더군요. 시간으로 DC기어모터의 각도를 제어해야 한다는게 만만치 않는 부분이네요. 처음부터 stepper Motor 두개를 달았다면 이문제는 쉽게 해결되었을텐데 말이죠

마무리


코딩상으로는 괜찮았는데 실제 주행에서는 DC기어모터의 각도제어랑 두 DC기어모터 직선주행 속도를 일치하니 않는 한계에서 정확한 지그재그 주행을 이루지 못했네요. 안타깝게 실패한 실험입니다.
하지만, 실패한 주행이 되었지만 지그재그 주행패턴 코딩 과정은 생각하는 만큼 표현은 되었네요. 오늘 post은 코딩과정만 보시기 바랍니다.

오늘의 핵심은 이 식입니다.

  //장애물 감지
  if(distance>0 && distance<10){
     int timetemp = maxTime - (millis() - timeVal);
     movePattern(); //다음라인방향전화패턴(실패함)
     timeVal = millis()-timetemp;
  }
  else if(millis() - timeVal > maxTime){
     movePattern(); //다음라인방향전환패턴(실패함)
     timeVal = millis();
  } 

movePattern() 주행패턴이 정교하지 못해서 주행은 실패했지만 위 식의 동작은 문제가 없습니다. 위 식의 내용은 주행중 초음파센서의 장애물이 감지되면 현재 주행라인에서 다음 주행라인으로 넘어가게 되고 장애물이 감지되지 않았다가 정상 주행하다가 maxTime(전체이동시간)만큼 주행하다가 다음 주행라인으로 넘어가게 된다는 코딩입니다.

실제 아두이노 RC카 주행이 실패했지만 그래도 제가 만든 이 식이 너무 아까워서 post를 하게 되었네요.

아두이노 RC카를 애완동물의 행동을 보고 그 행동의 패턴을 코딩으로 만들기 했는데 각도와 직선 주행에서 사실 결함이 많이 발생하네요. 나중에 기회가 되면 stepper Motor 두개로 좀 더 정교하게 제어를 해 봐야겠네요. 다양한 몇가지 패턴을 계속 post를 할까 했는데 코딩으로는 몇개 만들었는데 오늘 실제 주행에서 회전각과 일직선 주행에서 오차가 계속 영향을 줄 것 같아서 RC카 post는 이정도로 우선 마무리 할까 합니다. stepper Motor를 나중에 두개 구매해서 한번 도전해 봐야겠네요. 아직은 구매할 마음이 없지만요.

댓글()

[아두이노] 도망가는 아두이노 RC카 응용

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

[아두이노] 도망가는 아두이노 RC카 응용



오늘은 지난시간에 마무리로 이야기 했던 내용 중 하나를 선택하여 한번 실제로 동작을 테스트 해보면 좋을 것 같아서 이렇게 도전해 봤네요. 상황은 초음파센서 아두이노 RC카에 정면에 다가가면 초음파 아두이노 RC카가 다가온 방향의 반대로 일정거리 동안 도망가도록 주행패턴을 만들어서 실험을 하겠습니다.


1. 초음파센서 아두이노 RC카 회로도


  • 준비물 : L293D Motor Shield, DC Motor 2개, Servo Motor 1개, 초음파센서 1개, 외부전원 2개, 아두이노우노
  • 내용 : A4(Trig), A5(Echo)로 초음파센서에 연결하고 Servo Motor는 왼쪽 상당애 Servo Pin(-,+,Sig)에 연결한다. DC Motor 2개는 M3, M4에 연결합니다.



지난시간의 회로도 입니다.

2. 코딩



도망다니는 아두이노 RC카를 상상하여 그 상상을 코딩화 하는 과정을 이야기 할까 합니다. 상상의 내용은 다음과 같습니다.

[도망가는 아두이노 RC카 상상하기]

  1. 전방에 다가오는 사람이 있는지 확인한다.
  2. 초음파센서로 인지거리 내 감지되면 감지된 반대방향으로 도망간다.
  3. 아두이노 RC카가 후면(엉덩이) 흔들기
  4. 다시 180도 회전하여 전방에 다가오는 사람이 있는지 확인한다.
  5. 이 과정을 계속 반복한다.

1) 전방에 다가오는 사람이 있는지 확인한다.


void loop() {
  //초음파센서 회전
  servo.write(angle);
  delay(50);
  
  int distance = sonar.ping_cm();

  //다가오는 사람 확인
  if(distance>0 && distance<10){
    도망가기;   
  }
  //회전 각도
  if (angle == 140) state = -10;    
  else if (angle == 40) state = 10; 
  angle += state;
}

2) 초음파센서로 인지거리 내 감지되면 감지된 반대방향으로 도망간다.


  //다가오는 사람 확인
  if(distance>0 && distance<10){
    movePattern(); //도망가기
  }

여기서, distance의 거리가 10cm 미만이면 다가 온 걸로 간주 하고 movePattern()함수로 반대방향으로 도망을 가 볼까요.

void movePattern(){
     int timeVal=0;
     motor1.run(BACKWARD);
     motor2.run(BACKWARD);
     delay(500);   
    
    motor1.run(FORWARD);
    motor2.run(BACKWARD);
    timeVal=(180-(angle-90))*5.56;
    delay(timeVal);   
    
    motor1.run(FORWARD);
    motor2.run(FORWARD);
    delay(2000);                
}

정면에 다가온 각도 방향의 정 반대로 도망을 갑니다. timeVal은 1초당 1000일때 1도 당 5의 값으로 가정하여 계산하면 해당 방향을 기준으로 180도 회전을 할려면 다음과 같은 식으로 표현할 수 있습니다.


timeVal=(180-(angle-90))*5;

Servo Motor의 회전(angle)각 방향이 100도면 RC카의 전방 진행 방향이 90도 임으로 왼쪽 10도 각이면 위 식으로 계산을 하면

각도 = 180-(100-90) => 170

만약에, angle이 70도이면

각도 = 180-(70-90) => 200

그리고, 이 각도의 회전각 시간을 구하면 timeVal 변수에 5값을 곱하시면 해당 각도에 대한 아두이노 RC카의 회전각이 나오게 됩니다.

뒤로 회전이 된 후 2초동안 도망가게 됩니다.

motor1.run(FORWARD);
motor2.run(FORWARD);
delay(2000);  

이렇게 하면 계속 후진하는 동작이라서 뭔가 전진하거나 자신의 자리로 되돌아오는 그런 명령들이 필요 하다고 생각되실 꺼에요. 오늘 실험은 하나의 패턴 도망자 아두이노 RC카 이기 때문에 하나의 패턴에만 집중하기 위해서 코딩하고 싶지만 생략했습니다.

다른방식으로 표현하면

전방 기준으로 왼쪽은 오른쪽으로 회전하고 오른쪽은 왼쪽으로 회전하고 싶다면 코딩이 좀 길어집니다.

void movePattern(){
     int timeVal=0;
     motor1.run(BACKWARD);
     motor2.run(BACKWARD);
     delay(500);   
    if(angle>=90){
      motor1.run(FORWARD);
      motor2.run(BACKWARD);
      timeVal=(180-(angle-90))*5;
      delay(timeVal);   
    }
    else{
      motor1.run(BACKWARD);
      motor2.run(FORWARD);
      timeVal=(180+(angle-90))*5;
      delay(timeVal);         
    }
    motor1.run(FORWARD);
    motor2.run(FORWARD);
    delay(2000);                

if문으로 해서 90도 기준으로 왼쪽인지 오른쪽인지 나누게 됩니다. angle>=90 은 90도 이상이면 우회전하게 되고 90도 미만이면 좌회전을 하게됩니다. 이렇게 하면 timeVal 값을 구하는 식은 한개가 달라집니다.

90도 이상 => timeVal=(180-(angle-90))*5;
90도 미만 => timeVal=(180+(angle-90))*5;

보시면 90도 이상이면 180도에서 빼주지만 90도 미만은 180도에서 더해주면 됩니다. 식이 잘 이해가 안되면 angle(각) 값을 숫자로 대입해 보세요. 그러면 이해가 쉬울 듯 합니다.

3) 아두이노 RC카가 후면(엉덩이) 흔들기


도망 간 다음 아두이노 RC카가 후면을 엉덩이 흔드는 것처럼 흔드는 동작을 추가 해 볼까요.

void movePattern(){
  생략...
    
    for(int i=0;i<3;i++){
     motor1.run(FORWARD);
     motor2.run(BACKWARD);
     delay(300);
     motor1.run(BACKWARD);
     motor2.run(FORWARD);  
     delay(300);
    }
}

우로 0.3초 회전 한 뒤에 좌로 0.3초 회전한다. 이 과정을 3번 반복하면 엉덩이를 흔드는 동작처럼 표현 할 수 있겠죠. 만약에 꼬리 같은 걸 후면에 붙여 놓으면 좀 더 그럴싸 해 지겠죠.

4) 다시 180도 회전하여 전방에 다가오는 사람이 있는지 확인한다.


void movePattern(){
  생략...
    
  motor1.run(BACKWARD);
  motor2.run(FORWARD);
  timeVal=180*5;
  delay(timeVal); 
  motor1.run(RELEASE);
  motor2.run(RELEASE);
  angle=90;
  servo.write(angle);
  delay(100);   
}

좌회전으로 180도 회전하여 다시 아까 다가 온 방향으로 아두이노 RC카는 향하게 회전 시킨 후 아두이노 RC카는 정지 상태가 됩니다. 초음파센서는 다시 정면 90도 방향으로 향하게 합니다. 이렇게 해서 도망갔다가 다시 주변을 탐색을 시작하기 전까지의 동작이 끝나게 됩니다. loop()함수는 끝나고 다시 재호출 되어서 loop()함수는 주변에 사람이 있는지 확인하게 됩니다.

5) 이 과정을 계속 반복한다.


위 과정을 loop()함수에서 계속 반복하게 됩니다.

위 과정을 종합하여 코딩하면,

[기본소스]

#include <AFMotor.h>
#include <Servo.h> 
#include <NewPing.h>

AF_DCMotor motor1(3);
AF_DCMotor motor2(4);

Servo servo;
const int servoPin = 10;
const int TRIG = A4;
const int ECHO = A5;
const int MAX_DISTANCE = 100;
NewPing sonar(TRIG, ECHO, MAX_DISTANCE);

int speed=200;
int state = 10;
int angle = 90;

void setup() {
  //Serial.begin(9600);
  motor1.setSpeed(speed);
  motor2.setSpeed(speed);
  motor1.run(RELEASE);
  motor2.run(RELEASE);

  servo.attach(servoPin);
  servo.write(angle);
  delay(1000);
}

void loop() {
  //초음파센서 회전
  servo.write(angle);
  delay(50);
  
  int distance = sonar.ping_cm();
  
  //장애물 감지
  if(distance>0 && distance<10){  
     movePattern(); 
  }   
  //회전 각도
  if (angle == 140) state = -10;    
  else if (angle == 40) state = 10; 
  angle += state;
}

void movePattern(){
    int timeVal=0;
    motor1.run(BACKWARD);
    motor2.run(BACKWARD);
    delay(500);   
    
    //도망
    if(angle>=90){
      motor1.run(FORWARD);
      motor2.run(BACKWARD);
      timeVal=(180-(angle-90))*5;
      delay(timeVal);   
    }
    else{
      motor1.run(BACKWARD);
      motor2.run(FORWARD);
      timeVal=(180+(angle-90))*5;
      delay(timeVal);         
    }
    motor1.run(FORWARD);
    motor2.run(FORWARD);
    delay(2000);  
    
    //후면 흔들기
    for(int i=0;i<3;i++){
      motor1.run(FORWARD);
      motor2.run(BACKWARD);
      delay(300);
      motor1.run(BACKWARD);
      motor2.run(FORWARD);  
      delay(300);
  }
  
    //원위치 전방 보기
    motor1.run(BACKWARD);
    motor2.run(FORWARD);
    timeVal=180*5;
    delay(timeVal); 
    motor1.run(RELEASE);
    motor2.run(RELEASE);
    
    angle=90;
    servo.write(angle);
    delay(100);     
}

동작 하나하나 패턴을 추가해서 표현하니깐 movePattern()함수 내 동작 패턴이 꽤 길어 졌네요. 아두이노 RC카가 어떻게 움직이길 바라면 그 움직임을 하나씩 원하는 행동을 하도록 상상하고 그 상상을 이렇게 코딩화 하면 됩니다.

위 소스는 그냥 즉흥적으로 간단히 만든 거라서 사실 지져분한 코딩입니다. 맘에 들지 않는 코딩이라는 뜻이죠. 뭔가 축약된 코딩을 좋아하는데 이렇게 길게 늘려서 일일히 코딩하는 것을 싫어하기 때문에 맘에 들지는 않지만 대충 이런 느낌으로 코딩을 한다는 의미만 전달하기 위해서 그냥 이 소스로 마무리 합니다.

여러분들은 한번 다른 방식으로 도망자 아두이노 RC카를 만들어 보세요.

추가사항


위의 코딩만으로는 사실 문제점이 많습니다. 무조건 전방에 다가오는 물체에 대해서 도망만 가기 때문에 계속 후진하는 느낌의 코딩입니다. 또한 뒤로 도망을 갈때 보면 뒤에 장애물에 대한 감지가 빠져 있습니다. 이 부분을 추가할려면 loop()함수의 물체 감지 부분을 사용자 정의함수로 빼서 뒤로 도망가는 로직에 물체 감지 함수부분을 호출하는 식으로 코딩을 변경해야 합니다. 그리고 도망가는 2초간의 시간은 delay(2000) 하면 절대 안되고 delay()함수를 사용하지 않는 post [아두이노] delay()함수 안쓰고 delay 제어하기의 원리를 이용해서 2초간 도망가는 주행을 하는 동안 장애물을 감지하는 코딩으로 수정해야 합니다. 그리고, 그냥 도망으로만 끝내지 않고 다시 도망을 갔다가 물체 감지된 방향으로 조금직 다시 직립주행을 하면서 물체와의 거리를 측정하면서 자신이 있던 자리로 되돌아가게 코딩을 하면 좀 더 재밌는 코딩으로 변경 됩니다.

지금 열거한 내용만 코딩을 하더라도 수정해야 할 부분과 추가해야할 코딩이 늘어납니다. 그러면 오늘 전달하고자 하는 도망자 아두이노 RC카의 원리가 제대로 전달되지 않기 때문에 코딩에서는 실제 추가하지 않겠습니다. 이부분은 여러분들의 상상코딩에 맡기겠습니다.

3. 결과


코딩은 그럭저럭 되었는데 결과는 썩 마음에 들지 않네요. 원하는 결과는 얻지 못했습니다. 1초를 1000으로 해서 180도를 1초 회전으로 첨에 잡았는데 회전하니깐 180도 이상의 각도로 회전이 일어나서 댜략 눈 짐작으로 900 정도로 180도로 간주하여 회전을 시켰지만 그래도 회전각은 원한 각도로 정교하게 회전을 되지 않았네요. 2~3번의 주행 테스트를 하고 post를 한 거라 정교한 결과를 얻지 못해서 아쉽네요.


위 영상을 보시면 회전각 오차가 크고 도망가는 주행이 직립주행이 되지 않았네요. 건전지가 추가되고 무게중심이 보조 바뀌쪽으로 좀 더 가서 그런지 보조바퀴의 방향각에 의해서 회전이나 직립주행의 오차가 심하게 발생했네요. 아두이노 RC카의 몸체에 대해서 교정을해야하고 수학적으로 DC기어모터의 속도와 시간값을 통해 회전각을 정교하게 해야하는데 단순하게 눈짐작으로 실험을 한계 이런 오차결과를 만들어 내고 말았네요.

결과는 마음에 들지 않지만 코딩은 대충 어떤 느낌으로 설계하는지만 이해하는 시간이 되었으면 합니다. 기본적으로 이렇게 출발하고 조금씩 문제점에 대해서 해결 코딩을 해가면서 좀 더 정교한 프로그램을 만들어가는 것이죠.

마무리


원래 상상의 의도는 도망자 아두이노 RC카를 만들고자 했는데 느낌이 꼭 애완동물이 주인을 피해 도망갔다가 꼬리를 흔들고 다시 주인이 있는 방향으로 다시 바라보는 느낌의 코딩이 되어버렸네요.

이 느낌으로 좀 더 추가해 볼까 하는 생각도 드는데 아직 결정은 안했습니다. 따라다니는 아두이노 RC카를 다음편에 이야기 할까 그냥 여기서 끝낼까 고민을 좀 해봐야 겠네요.

코딩은 아직 안했는데 상상을 잠깐 해보고 결정해야 겟네요.


댓글()

[아두이노] 아두이노 RC카 자율주행(장애물피하기)

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

[아두이노] 아두이노 RC카 자율주행(장애물피하기)



오늘은 몇일동안 포스트한 장애물을 피하는 자율주행 테스트 해보는 시간으로 내용을 채우고자 합니다. 좀 더 정교한 자율주행 코딩을 할까도 생각 했지만 처음은 단순한 장애물을 피하는 자율주행을 보여드리는 것이 좋을 것 같아서 종합 코딩은 간단하게 표현하여 자율주행의 의미를 전달하고자 합니다.


우선 초음파 아두이노 RC카를 사진으로만 보면 정확히 구조를 이해할 수 없으니 간단히 fritzing으로 회로도를 보여드리고 간단히 장애물피하기 자율주행 코딩을 보여드리겠습니다.

1. 초음파센서 아두이노 RC카 회로도


  • 준비물 : L293D Motor Shield, DC Motor 2개, Servo Motor 1개, 초음파센서 1개, 외부전원 2개, 아두이노우노
  • 내용 : A4(Trig), A5(Echo)로 초음파센서에 연결하고 Servo Motor는 왼쪽 상당애 Servo Pin(-,+,Sig)에 연결한다. DC Motor 2개는 M3, M4에 연결합니다.



위 사진의 아두이노 RC카의 회로도는 아래 그림과 같이 구성되어 있습니다.


위 그림에서 외부 전원을 두개로 분리해서 공급합니다. 참고포 점퍼핀 덮개를 빼주면 아두이노와 Motor Shield의 전원을 나눌 수 있습니다. 그림이 좀 복잡해 보일 수 있지만 내용에 자세히 핀 연결 설명이 되어 있으니깐 해당 위치에 부품의 선을 연결해주시면 됩니다.

2. 코딩



이전 post에서 장애물을 감지하는 방법과 장애물을 피하는 패턴을 살펴 보았습니다. 여러개를 설명했는데 그중에 각각 한개씩 간단한 표현으로 코딩을 하겠습니다.

[기본소스]

#include <AFMotor.h>

AF_DCMotor motor1(3);
AF_DCMotor motor2(4);

void setup() {
  motor1.setSpeed(200);
  motor2.setSpeed(200);
  motor1.run(RELEASE);
  motor2.run(RELEASE);
}

void loop() {  

  초음파센서 장애물 감지;

  motor1.run(FORWARD); //전진
  motor2.run(FORWARD);
  delay(1000);
}

1) 서보모터+초음파센서 장애물 감지


[코딩 순서]

  • 40~140도 사시를 10도씩 회전하면서 초음파센서로 장애물를 측정한다.
  • 장애물 감지 거리를 15cm 미만일 때 감지로 간주한다.
void loop() {
  //초음파센서 회전
  servo.write(angle);
  delay(50);
  
  int distance = sonar.ping_cm();

  //장애물 감지
  if(distance>0 && distance<15){
     movePattern(); 
     //Serial.println(distance);
     motor1.run(FORWARD);
     motor2.run(FORWARD);  
  }   
  //회전 각도
  if (angle == 140) state = -10;    
  else if (angle == 40) state = 10; 
  angle += state;
}

2) 장애물 피하기 패턴


[코딩 순서]

  • 장애물 감지시 0.5초 동안 후진한다.
  • 전방 90도를 기준으로 90이상이면 우회전 90미만 좌회전 시킨다.
  • 장애물 피하기 회전이 끝나면 다시 전진 주행한다.
  • 초음파센서는 방향은 90도로 위치시킨다. (90도 기준으로 다시 10도씩 변화를 시키기 위해서)
void movePattern(){
     motor1.run(BACKWARD);
     motor2.run(BACKWARD);
     delay(500);   
    if(angle>=90){
      motor1.run(FORWARD);
      motor2.run(BACKWARD);
      delay(500);   
    }
    else{
      motor1.run(BACKWARD);
      motor2.run(FORWARD);
      delay(500);   
    }
    angle=90;
    servo.write(angle);
    delay(100);
}

3) 기본소스+장애물감지소스+장애물피하기 패턴소스


위 세가지 소스를 합치면 다음과 같습니다. 참고로 몇가지 다듬어서 코딩을 했습니다.

#include <AFMotor.h>
#include <Servo.h> 
#include <NewPing.h>

AF_DCMotor motor1(3);
AF_DCMotor motor2(4);

Servo servo;
const int servoPin = 10;
const int TRIG = A4;
const int ECHO = A5;
const int MAX_DISTANCE = 100;
NewPing sonar(TRIG, ECHO, MAX_DISTANCE);

int speed=200;
int state = 10;
int angle = 90;

void setup() {
//  Serial.begin(9600);
  motor1.setSpeed(speed);
  motor2.setSpeed(speed);
  motor2.run(RELEASE);
  motor2.run(RELEASE);

  servo.attach(servoPin);
  servo.write(angle);
  delay(1000);

  //Start
  motor1.run(FORWARD);
  motor2.run(FORWARD);  
}

void loop() {
  //초음파센서 회전
  servo.write(angle);
  delay(50);
  
  int distance = sonar.ping_cm();

  //장애물 감지
  if(distance>0 && distance<15){
     movePattern(); 
     //Serial.println(distance);
     motor1.run(FORWARD);
     motor2.run(FORWARD);  
  }   
  //회전 각도
  if (angle == 140) state = -10;    
  else if (angle == 40) state = 10; 
  angle += state;
}
void movePattern(){
     motor1.run(BACKWARD);
     motor2.run(BACKWARD);
     delay(500);   
    if(angle>=90){
      motor1.run(FORWARD);
      motor2.run(BACKWARD);
      delay(500);   
    }
    else{
      motor1.run(BACKWARD);
      motor2.run(FORWARD);
      delay(500);   
    }
    angle=90;
    servo.write(angle);
    delay(100);
}

setup()함수에서 초기화 작업을 하는데 젤 먼저 DC기어모터의 속도를 200으로 세팅하고 각 Motor를 RELEASE(해제 or 정지)상태로 둔다. 그리고 나서 주행전 Servo Motor의 초기 위치는 90도 회전 시켜 초음파센서가 전방90도를 바라보게 한다. 그 다음 1초동안 준비 상태로 있다가 FORWARD(전진)명령으로 아두이노 RC카가 출발하게 됩니다.

loop()함수는 angle(각도)에 위치로 회전을 0.05초 간격으로 합니다. 회전이 될 때 초음파센서(sonar)의 거리를 측정하여 distance에 저장합니다. 이 값이 0보다 크거나 15cm보다 작은 값일 때 장애물을 감지한 걸로 감주합니다. 0의 값은 MAX_DISTANCE의 제한최대거리 값을 넘게 되면 0으로 반환되기 때문에 0의 값이 주행 중에 일정 간격으로 발생하게 됩니다. 그렇기 때문에 0보다 크거나 15cm보다 작은 값으로 if문으로 체크하는 조건식을 만든 것이죠. 이렇게 15cm 미만일 때 장애물 감지 판정을 내리게 되면 if문 이하 문장을 수행 하게 됩니다. 이때 장애물 피하기 패턴을 코딩하면 됩니다 movePattern()함수로 장애물을 피하는 동작을 코딩해 놨는데 이 방식으로 피하고 나면 다시 motor1, motor2은 FORWARD(전진)으로 게속 자율주행을 하게 됩니다.

여기서, movePattern()함수를 살펴보면 먼저 0.5초 동안 후진을 하고 후진 한 후에 Servo Motor가 회전한 현재 각(angle)의 값이 90보다 큰거 아니면 작은 가로 나누게 되는데 이때 90도보다 크면 좌측에 장애물을 감지했기에 우회전 명령문을 0.5초 동안 90도 우회전을 하게 되고 90도보다 작으면 우측에 장애물을 감지했기에 좌회전 명령문으로 0.5초 동안 90도 좌회전을 하게 됩니다. 그리고 다시 전진 주행을 하기 전에 초음파센서의 방향 위치를 초기화 상태로 전방 90도 위치로 회전시켜놓습니다.

대충 전체의 소스에 대해 설명을 했는데 이해하셨는지 모르겠네요.

만약, 다른 방식으로 접근하고 싶다면 장애물 감지 코딩 부분을 다른 감지 주행패턴을 선택해서 붙여 넣으시면 되고요. 장애물 피하기 패턴도 movePattern()함수 내부의 코딩을 원하는 패턴으로 만들어서 넣으시면 됩니다.

3. 결과


그냥 전방의 장애물을 손으로 표현해서 손으로 막을 때 이 손을 아두이노 RC카가 장애물로 감지하고 초음파센서가 감지한 각도에 따라 좌/우회전을 하는 짧게 촬영한 영상입니다.


이렇게 간단하게 장애물 피하기로 자율주행을 출발합니다.

위 코딩을 좀 더 정교하게 하려면 다음과 같이 해야 합니다.

void loop(){
  //장애물 감지
  if(distance>0 && distance<15){
     movePattern(); 
  }  
}   
void movePattern(){
     motor1.run(BACKWARD);
     motor2.run(BACKWARD);
     delay(500);   
    if(angle>=90){
      motor1.run(FORWARD);
      motor2.run(BACKWARD);
      delay(회전각도시간값);   
    }
    else{
      motor1.run(BACKWARD);
      motor2.run(FORWARD);
      delay(회전각도시간값);   
    }
    motor1.run(FORWARD);
    motor2.run(FORWARD);    
        
    angle=90;
    servo.write(angle);
    delay(100);
}

여기서, 회전각도시간값은 여러분들이 가지고 있는 아두이노 RC카의 바퀴힐과 속도를 기준으로 1초(1000)에서 delay값 100을 기준으로 몇도 회전되는지 체크했다가 정확하게 회전각도시간값을 넣어주세요. 그리고 회전을 시킨 후 바로 전진 주행으로 바꿔 주시면 좀 더 정확하게 회전 후 전진 주행이 됩니다. 사실 촬영할 때는 loop()함수에다가 넣어서 간단히 movePattern()함수에서는 회전 주행만 표현했는데 마지막 초음파센서의 90 회전 초기화에서 0.1초 동안의 회전이 더 일어나는 단점을 가지게 되더군요. 그러면, 회전각도시간값으로 정확히 회전을 시키더라도 초기화 0.1초동안 회전이 더 발생하는 문제가 생길 수 있습니다. 단순하게 장애물 피하기 코딩에서는 0.1초 차이는 티도 안나기 때문에 무시하고 촬영을 했지만 post을 작성하면서 이 코딩부분을 수정해서 올릴까도 했지만 촬영은 이전 소스를 기반으로 주행 시켜서 그냥 약간 부족한 상태로 수정 없이 올리게 되었네요. 하지만 이렇게 추가로 이야기 하고 넘어가야 할 것 같아서 좀 더 내용을 채우게 되었네요.

motor1.run(FORWARD);
motor2.run(FORWARD);   

아무튼 이 명령이 어느 위치에 있느냐에 따라 결과는 달라지게 됩니다. 단순한 코딩은 무시 할 정도로 표현해도 되지만 정교한 코딩에서는 명령문의 위치에 따라 딜레이 0.1초 차이도 회전 오류각을 발생할 수 있기 때문에 코딩을 완료되더라도 완료된 코딩의 흐름을 머리속에서 상상하면서 문제가 없는지 체크를 꼭 하셔야 합니다.

마무리


위 코딩을 보면 그렇게 어려운 코딩이 아닙니다. 재밌는 것은 이 원리를 이해하시면 또다른 다양한 표현을 할 수 있습니다. 장애물을 피한다면 아두이노 RC카가 도망다니는 RC카로 표현 할 수 있습니다. 가령, 전방의 특정 각도의 장애물이 감지되면 그 장애물의 각도을 기준으로 180도 회전해서 뒤로 도망가는 RC카를 만들 수 있습니다. 인간과 RC카가 이 원리로 접근하게되면 인간이 아두이노 RC카를 추적하고 아두이노 RC카는 도망자가 됩니다.

아니면, 주행을 규칙적으로 지그재그로 표현을 한다면 지그재그 주행을 할 때 전방의 초음파센서가 장애물을 감지하면 진행 방향에 있는 장애물을 피하기 위해서 진행 라인의 주행을 포기하고 다음 라인의 주행을 이여간다면 로봇청소기와 같은 주행을 할 수 있게 됩니다.

또 다른 상상을 하면 아두이노 RC카를 랜덤 주행을 일정 범위안에서 수행되게 해놓고 있다가 초음파센서의 장애물 감지 범위에 인간이 다가갔을 경우 일정 거리까지 아두이노 RC카가 따라오도록 주행패턴을 만들게 되면 어떤 느낌의 아두니오 RC카가 될까요. 바로 애완동물 아두이노 RC카로 표현이 가능해 집니다.

간단히 장애물 감지와 장애물 피하는 동작의 원리를 곰곰히 생각해보니깐 방금 이야기 했던 상상들이 떠오르더군요. 위에서 설명한 주제들의 원리는 전부 동일한 원리 입니다. 장애물 감지와 장애물 피하기에서 장애물 피하기를 역발상으로 장애물 따라오기로 생각의 관점을 바꾸면 이렇게 또 다른 결과가 나올 수 있습니다. 재밌는 아두이노 RC카를 만들 수 있겠죠.

여러분들도 한번 이 원리를 기반으로 나는 어떤 상상을 할 수 있을지 상상의 나래를 펼쳐보셨으면 합니다.


댓글()

[아두이노] 아두이노 RC카 초음파센서로 장애물 피하기 패턴

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

[아두이노] 아두이노 RC카 초음파센서로 장애물 피하기 패턴



지난 시간에는 초음파센서로 장애물을 감지하는 방법들에 대해서 간단히 살펴 보았습니다. 오늘은 감지 했을 때 피하는 방법에 대해 한번 이야기 하고자 합니다. 아두이노 RC카는 주행 중 전방에 장애물이 감지하면 후진을 할 것인지 좌회전을 할 것인지 아니면 우회전을 할 것인지를 결정해야 합니다. 여러분들은 어떤 선택을 하실 건지 한번 머리속에서 상상을 해보세요. 현재 여러분들이 서 있는 위치에서 앞으로 걷다가 앞에 벽이 나타난다면 나는 어느 방향으로 벽을 피해 이동할지를 상상하며 한번 걸어보세요. 벽이 나타나고 벽을 피해서 방향전환하고 나서 다시 걷는 상상을 하거나 아니면 실제 걸어보시면서 걷는 과정을 기록해 주세요. 이 행동의 기록이 아두이노 RC카의 주행 패턴이 됩니다. 아두이노 RC카는 이 주행 패턴을 통해서 자율주행을 하게 됩니다. 어떤 느낌인지 아시겠지요.


지금부터 장애물 감지 센서인 초음파센서를 지난 시간에 배운 방식 중에 하나를 선택해서 실험해야 하는데 위 사진을 보면 서보모터로 회전되는 초음파센서로 전방에 하나 배치했기 때문에 이 방식을 통해 장애물을 감지 하고 장애물을 피하는 패턴들을 만들겠습니다.

1. 초음파센서로 장애물 감지 시 피하기 패턴



위의 선행 학습을 꼭 하시고 오셔야 합니다. 그래야 아래 내용을 쉽게 이해 하실 수 있습니다.

1) 전방 장애물 피하는 기본 방향 패턴


지난 시간에 몇가지 초음파센서로 장애물 감지에 대해서 살펴 보았습니다. 장애물 감지 중 첫번째 회전 초음파센서로 장애물 감지를 기준으로 한번 패턴을 만들어 보도록 할까요.

주행 중 회전 초음파센서로 장애물을 감지 상황은 다음과 같습니다.


주행 중에 전방에 장애물이 감지하는 한다고 상상해 봅시다. 이 상황에서 여러분들은 어떤 주행 패턴을 만드시겠습니까?


좌회전 아니면 우회전 그것도 아니면 후진 중 어떤 주행패턴을 하실지 머리속에서 그려 보세요.

#include <AFMotor.h>

AF_DCMotor motor1(3); //왼쪽 모터
AF_DCMotor motor2(4); //오른쪽 모터

두개의 DC기어모터를 제어할 Motor가 있을 때

  • motor.run(FORWARD) : 전진
  • motor.run(BACKWARD) : 후진
  • motor.run(RELEASE) : 해제

모터의 기본 동작함수인데 이 함수를 통해서 장애물을 피하는 패턴을 만듭니다.

if(장애물 감지){
  장애물 피하기 패턴;
}

장애물 감지 시 제자리에서 좌/우 회전을 시킨 후 회전 된 방향으로 주행을 계속 진행하도록 주행패턴을 만들어 보도록 합시다. 단, 90도 회전은 0.5초동안 회전하고 180도 회전은 1초동안 회전이라고 가정하고 상상 코딩을 합니다. Motor의 속도에 따라 시간에 따른 회전각은 달라집니다. 개념을 잡기 위해서 0.5초은 90도 이고 1초가 180도라고 상상한 것이기 때문에 이대로 실제 코딩하시면 안됩니다. Motor 속도와 시간에 대한 회전각도를 수학적으로 계산하시거나 단순하게 속도에 따른 시간별 각도를 일일히 체크해서 각도를 가늠하시거나 둘 중 하나를 선택하셔서 나중에 실제 만들어 보세요.


[90도 우회전 후 전진]

motor1.run(FORWARD);
motor2.run(BACKWARD);
delay(500);   
motor1.run(FORWARD);
motor2.run(FORWARD);

[90도 좌회전 후 전진]

motor1.run(BACKWARD);
motor2.run((FORWARD););
delay(500);   
motor1.run(FORWARD);
motor2.run(FORWARD);

이렇게 하면, if문으로 장애물 감지되면 좌/우로 90도 회전 시킨 후 그 방향으로 전진하게 됩니다. 장애물 피하기 주행 패턴이 단순하죠.

다음으로, 뒤로 회전 한다면 좌/우로 회전 방향은 상관 없이 180도 회전을 시키면 됩니다.


[180도 뒤로 회전 후 전진] 우방향을 기본으로 잡았을 경우

motor1.run(FORWARD);
motor2.run(BACKWARD);
delay(1000);   
motor1.run(FORWARD);
motor2.run(FORWARD);

대충 이런 느낌이 됩니다. 1초가 정확히 180도 회전이 아닙니다. 가정한 것이고 DC기어모터 회전 속도에 따라서 회전각은 달라집니다. 가정하에서 코딩한 것이기 때문에 감안하시고 보세요.

이렇게 해서 간단히 장애물을 피해서 주행을 할 수 있게 되었습니다.

2) 전방 장애물을 피할 때 문제점


방금 세가지 피하는 방식에서 문제가 발생할 수 있습니다. 제자리에서 아래 그림처럼 DC기어모터가 회전하면 바퀴를 기준으로 몸체가 돌아가게 됩니다. 몸체가 돌아갈 때 장애물과의 충돌이 발생 할 수 있습니다.


제자리 좌/우회전을 시키거나 뒤로 180도 회전 시킬때 장애물 거리 감지가 짧을 경우 아래와 같은 상황이 발생 할 수 있습니다.

[우회전]


[180도 회전]



위 그림같은 상황 때문에 주행 중 진행 방향에 장애물이 감지 되었을 때 다음 장애물 피하기 동작 패턴을 만들려면 감지한 거리과 아두이노 RC카의 몸체의 크기를 고려해야 합니다. 그리고 피하는 행동을 할 때에도 주변 환경의 장애물과의 충돌 상황도 고려해야 합니다.

[해결 방법]

  • 첫번째, 장애물 감지 거리를 좀 더 길게 잡으면 해결 됩니다. 아두이노 RC카가 충분히 회전 할 수 있는 거리만큼을 장애물 감지 거리로 설정해 놓으면 장애물 감지 후 바로 회전을 하더라도 정상적으로 회전 할 수 있습니다.
  • 두번째, BACKWARD(후진)을 일정 거리만큼 시킨 뒤 회전을 시키면 됩니다. 하지만, 이 경우에는 또 다른 문제점으로 후진 후 회전시 아래 그림처럼 측면에 장애물이 있을 경우 충돌 가능성이 있습니다.

위 그림처럼 문제가 생기면 후면에 초음파센서를 부착하여 후진 시 뒷면에 장애물을 감지하면 해결 할 수 있겠죠. 그런데 이부분은 코딩화 하지 않겠습니다 계속 상황을 만들어 가면 이야기가 끝도 없기 때문에 의미만 전달하기 위해서 이정도로 마무리 합니다.

장애물을 피하는 주행 패턴을 만들 때 처음에 제가 설명한 세가지 주행 패턴을 기반으로 상상코딩을 한 뒤에 주행 패턴을 변화시키십시오. 만약에, 자신이 만든 주행패턴에 문제가 생기면 그 문제 해결을 위한 주행 패턴을 만들고 기존 소스에 덧붙여가며 코딩을 늘려가시면 문제를 충분히 해결 할 수 있게 됩니다. 처음은 단순한 주행패턴으로 머리속에서 정리하고 그 주행 패턴에서 하나씩 새로운 주행 패턴을 만들어가면 좀 더 정교한 장애물 피하기 패턴을 만들 어 가면 자율주행하는 재밌는 아두이노 RC카를 만들 수 있습니다.

2. 회전 초음파센서로 장애물 감지 후 피하기 패턴 - I



위 그림처럼 초음파센서는 전방에 일정 각도로 좌/우 회전을 하면서 각도마다 거리를 측정하게 됩니다. A, B, C로 세부분으로 나누었지만 만약, 40~140도 사이의 각도를 10도 간격으로 하면 11개의 각도의 장애물 감지 거리를 측정하게 됩니다. 11개의 각도가 나올 때 전방 중앙을 90도 각도로 하면 좌/우 5개의 각도로 장애물 감지 거리가 측정 할 수 있습니다.

이렇게 각도별로 장애물이 감지 되었을 때 어떻게 상상코딩을 할까요. 정중앙을 90도기준으로 장애물이 감지된 초음파센서 각도에서 반대 방향으로 회전시켜 장애물을 피하는 간단한 방법이 있습니다.

위 그림에서 B가 90도 기준으로 A은 140도 방향이고 C은 40도방향이라고 한다면 A방향은 좌측이고, C방향은 우측으로 나눌 수 있습니다. 이때, A방향의 장애물이 감지 되었을 때 우측으로 진로 C방향으로 진행하도록 합니다. C방향 우측에서 장애물이 감지되면 좌측으로 진로 C방향으로 진행하게 하면 간단한 피하기 패턴을 만들 수 있습니다.

[A방향 장애물 발견 시 90도 우회전 후 전진]

motor1.run(FORWARD);
motor2.run(BACKWARD);
delay(500);   
motor1.run(FORWARD);
motor2.run(FORWARD);

[B방향 장애물 발견 시 90도 좌회전 후 전진]

motor1.run(BACKWARD);
motor2.run((FORWARD););
delay(500);   
motor1.run(FORWARD);
motor2.run(FORWARD);

이렇게 진로 방향을 바꾼다면 장애물 감지는 아래와 같이 코딩을 할 수 있겠죠.

if(장애물거리<15){
  if(초음파각도>90){
      [A방향 장애물 발견 시 90도 우회전 후 전진];
  }
  else{
      [B방향 장애물 발견 시 90도 좌회전 후 전진];
  }
}

이렇게 단순하게 표현 할 수 있습니다.

추가로

전방, 좌/우측 방향의 장애물이 나타 났을 때 각도별 상황 구간을 잡아 놓고 아두이노 RC카를 좀 더 세부적으로 아래와 같이 if문으로 나누어서 각도별로 회전 주행 패턴을 만들 수 있습니다. 아래는 3구간으로 나눴지만 더 나눌 수 있고 또는 각 나눈 구간에서 다시 if조건문으로 해당 구간의 각도 범위에서 좀 더 각도를 나눠서 제어 방식을 변경할 수 도 있습니다. 이것은 여러분들의 상상력에 달려 있으니깐 한번 상상해 보세요.

if(A상황일때) 우회전;
else if(B상황일때) 좌/우회전 결정;
else if(C상황일때) 좌회전;

3. 회전 초음파센서로 장애물 감지 후 피하기 패턴 - II


위에서는 단순하게 장애물이 발견 된 시점에서 제자리에서 방향전환을 시키고 그다음 주행을 진행 했습니다. 뭔가 주행 중에 멈춰서 회전을 하기 때문에 끊기는 느낌으로 주행이 됩니다.


위 그림처럼 부드럽게 반원을 그리 듯 곡선 주행을 시키는 주행 패턴을 만들고 싶지 않으신가요. 이 방식은 어떤 주행패턴 일까요.

지난 [아두이노] 아두이노 2륜 RC카 주행 패턴 실험에서 주행패턴을 여러가지를 설명했는데 다음 주행패턴 코딩으로 표현을 할 수 있습니다.

motor1.setSpeed(130);
motor2.setSpeed(200);
motor1.run(FORWARD);
motor2.run(FORWARD);

바로, 이 주행패턴 코딩으로 곡선 주행을 하게 됩니다. 좌회전을 하고 싶다면 좌측 DC기어모터의 속도를 낮추게 되면 좌회전을 위 그림처럼 하게 되고 우회전을 하겠다면 우측 DC기어모터의 속도를 낮추면 그 방향으로 회전이 이뤄집니다. 실험에 사용하는 2륜 아두이노 RC카의 방향전환은 해당 DC기어모터의 속도에 따라서 방향을 전환하면서 주행할 수 있습니다. 도로주행과 같은 느낌의 주행이 되겠죠.

하지만, 아두이노 RC카는 이 방식만 있는게 아닙니다. 앞바퀴에 핸들식으로 서보모터를 부착해서 좌/우회전을 시킬 수 있는 방식이 있습니다. RC카 완제품을 사시면 앞바퀴 핸들식으로 무선조정하는 제품들을 많이 보셨을 꺼에요. 이방식에서는 서보모터가 45도가 수평일 때 90도는 좌회전이고 0도는 우회전으로 방향으로 조정하고 뒷바퀴는 그냥 RC카의 속도와 전진/후진 기능을 담당하면 됩니다.


servo.write(90);
motor1.run(FORWARD);
motor2.run(FORWARD);

이렇게 코딩을 바꾸면 됩니다.

마무리


대충 장애물 감지 했을 때 몇가지 패턴들을 만들어 보았습니다. Post [아두이노] 아두이노 2륜 RC카 주행 패턴 실험 의 내용에서 별로 바뀐 부분은 없습니다. 그냥 장애물 감지했을 때 상황을 두고 몇가지 패턴만 만들었을 뿐입니다. 상황을 그림으로 그리고 나서 그 그림을 기반으로 DC기어모터의 바퀴 회전을 어떤 형태로 회전 시킬 건지만 여러분들이 결정해서 패턴을 만들어 내면 됩니다.

  • motor.run(FORWARD) : 전진
  • motor.run(BACKWARD) : 후진
  • motor.run(RELEASE) : 해제

이 함수 3개를 이용하여 초음파센서로 장애물이 감지되면 각 DC기어모터를 FORWARD, BACKWARD, RELEASE 중 하나를 선택하여 장애물를 피하는 주행 패턴을 만들면 됩니다.

처음에는 이렇게 단순하게 장애물에 대해서 난 이렇게 피할꺼야! 아니면 저렇게 피할꺼야! 하면서 쉽게 만들면 됩니다. 처음부터서 정교하게 실제 차처럼 주행시켜야하지 하면 단순하 RC카 주행도 못시킬 가능성이 큽니다. 처음은 단순하게 무조건 쉽게 아두이노 RC카 주행을 시키면서 거기서 문제점이 발견되면 그 문제에 대해 다시 또 다른 주행 패턴을 만들고 하면서 조금씩 살을 붙여가면서 하셔야 정교한 주행이 되는 아두이노 RC카를 만들어 낼 수 있습니다.

오늘 Post도 처음에는 기조척으로 90도 기준으로 해서 좌/우 방향 회전하여 주행 패턴을 만들었는데 만들고 나서 Post를 쓰다보니깐 쓰는 도중에 상상코딩을 하게 되면서 주행 패턴에 대해 내용이 추가되었네요. 몇가지 더 내용이 있었는데 너무 길게 쓰는 것은 좀 그래서 중간에 멈췄네요. 다른 상황과 주행 패턴이 있었는데 대충 이런식으로 장애물을 피하는 주행패턴을 만들 수 있다는 의미를 전달하고 여러분들의 상상력에 맞기는게 나을 것 같아서 이정도로 마무리 했네요.

다음 Post에서 장애물 감지와 장애물 감지 했을 때 어떤 주행 패턴을 선택할지는 결정을 못했네요. 90도 기준으로 좌/우 방향 회전이 가장 유력하지만 너무 복잡한 주행 코딩은 안하고 단순하게 의미전달하는 주행 코딩으로 소개할 것 같습니다. 아무튼 여러분들은 오늘 제가 상상한 주행 패턴을 보시고 여러분들도 직접 장애물이 이렇게 배치 되어 있다면 난 이런 주행을 시켜봐야지 하고 상상을 하는 시간을 가졌으면 합니다.

댓글()

[아두이노] 초음파센서 아두이노 RC카 장애물 감지 방법

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

[아두이노] 초음파센서 아두이노 RC카 장애물 감지 방법



오늘은 초음파센서로 전방에 장애물을 감지하는 방법에 대해서 이야기를 할까 합니다. 우선, 주행하는 전방에 장애물이 등장 했을 때 여러분들이 제작한 RC카가 어떤 주행을 시킬 것인지 상상을 한번 해봅시다. 주행과 장애물 감지에 대해 잘 상상이 안될 수 있습니다. 그런데 주행과 장애물 감지 원리를 알면 그렇게 어렵지 않습니다. 주행과 장애물 감지에서 주행은 그냥 전진주행을 하게 하고 전진 주행 중 장애물에 대해 일정시간 단위로 초음파센서가 측정하게 하면 됩니다. 이 두가지 과정을 loop()함수에서 반복하게 하면 아두이노가 전진 주행중에 실시간으로 계속 장애물을 감지하게 되겠죠. 그리고 장애물이 감지 되었을 때 주행 방향을 바꿔주는 코딩을 하면 아두이노 RC카 자율주행이 됩니다. 복잡하게 출발하지 말고 쉽게 전진 중행을 하면서 초음파센서로 거리를 측정하여 장애물을 감지한다는 개념만 잡고 출발하시면 됩니다.


그러면, 간단히 상황을 설정하고 장애물 감지에 대해 살펴보도록 합시다.

1. 아두이노 RC카의 기본 주행



참고 자료를 보시면 주행 패턴에 대한 내용입니다. RC카가 주행을 하기 위한 기본 소스 코딩이 있는데 주행 패턴에 대해 사전학습을 하셨으면 합니다. 우선 아두이노 RC카의 움직임을 먼저 머리속에서 개념을 잡아놓고 출발하시면 쉽게 코딩 할 수 있습니다.

자율 주행은 전진 주행을 기본 베이스로 합니다.

#include <AFMotor.h>

AF_DCMotor motor1(3);
AF_DCMotor motor2(4);

void setup() {
  motor1.setSpeed(200);
  motor2.setSpeed(200);
  motor1.run(RELEASE);
  motor2.run(RELEASE);
}

void loop() {  

  초음파센서 장애물 감지;

  motor1.run(FORWARD); //전진
  motor2.run(FORWARD);
  delay(1000);
}

이렇게 기본 베이스 코딩이 끝났습니다. 실행을 하면 무조건 전진 주행을 하게 됩니다. 지난 RC카 post에서 위 소스에 대한 설명을 했기 때문에 간단히만 재정리 차원으로 설명하면 motor1(3), morot2(4)로 모터쉴드에서 M3, M4의 위치의 핀으로 DC기어모터 두개를 제어하는데 속도는 setSpeed(200)으로 스피드가 200이고 FORWARD(전진), BACKWARD(후진), RELEASE(해제) 중에서 loop()함수에서 무한 반복 FORWARD(전진)을 한다는 로직입니다.

딱 한줄로 정리하자면 전진만 하는 RC카라고 생각하시면 됩니다.

자율주행 기본 베이스 코딩은 끝났습니다. 이 소스를 기반으로 초음파센서로 장애물을 감지하는 방법들을 살펴 볼까요.

2. 초음파센서로 장애물 감지


초음파센서로 장애물을 감지하는 방식은 엄청 많습니다. 지금 소개하는 방식 외에도 더 많지만 생각나는 것만 몇개 간단히 설명을 할까 합니다.

1) 회전 초음파 센서로 장애물 감지



위 그림처럼 직진으로 주행하다가 장애물이 나타났을 때 초음파센서가 장애물을 어떤 식으로 감지 해야 할까요. 초보적 접근 방법으로는 두가지 방식으로 나누어 살펴 볼 수 있습니다. 주행 중 전방, 좌/우측 방향의 장애물 감지하는 방식과 주행 중에는 전방 장애물만 감지하다가 장애물 발견시 주행 중단 후 좌/우측 방향 장애물 감지하는 방식으로 나누어 살펴 볼 수 있습니다.

주행 중 전방, 좌/우측 방향의 장애물 감지



위 그림처럼 아두이노 RC카는 전방으로 진행하면서 서보모터가 부착된 초음파센서가 전방 일정 각도 범위로 왔다 갔다 회전하면서 장애물을 감지합니다. 이 방식은 전방, 좌/우측 방향으로 3가지 방향을 실시간으로 측정하여 장애물이 감지했을 때 이 3가지 방향의 값을 기준으로 장애물을 피하기 위한 RC카 주행 패턴을 만들 수 있습니다. 주행 중에 초음파센서가 장애물 감지하기 위해 거리 측정을 하고 장애물이 감지 된 측정 각도에 따라 진행 방향을 바꿀 수 있기 때문에 장애물이 감지된 각도에 따라 적절하게 방향을 바꿀 수 있게 할 수 있습니다.

void loop() {
  //회전
  servo.write(angle);
  delay(50);  

  //거리측정
  int distance = sonar.ping_cm();
  
  //장애물 감지
  if(distance<15){
      각도에 따른 장애물 피하기 동작;  
  }
  
  //회전 각도
  if (angle == 140) state = -10;    
  else if (angle == 40) state = 10; 
  angle += state;

  전방주행;
}

위 소스는 지난시간에 정상적으로 초음파센서가 0~180도 회전되는지 테스트한 코딩을 약간 수정한 소스입니다. 이 코딩을 기반으로 0.05초 간격으로 회전하면서 거리를 측정하면 됩니다. 회전 각도는 10도씩하여 40~140도 사이를 11번을 측정하게 됩니다. 1도씩 안하고 10도씩 한 이유는 사실 장애믈을 측정하기 위해서 1도씩 하면 110번을 측정해야하고 왕복 220번을 0.05초 간격으로 측정을 한다면 꽤 긴시간이 걸리기 때문에 실시간 대응이 어렵습니다. 짧게 돌면서 원하는 동작을 수행하기에는 10도정도가 가장 이상적이네요. 더 큰 각도로 제어해도 됩니다, 그냥 90도, 135도, 45도 이렇게 세각도만 측정해도 됩니다. 선택은 여러분의 마음입니다.

주행 중에는 전방 장애물만 감지하다가 장애물 발견시 주행 중단 후 좌/우측 방향 장애물 감지



아두이노 RC카가 직진 주행하는 중 전방 방향의 장애물만 측정합니다. 그러다가 장애물이 발견 되면 위 그림에서 오른쪽 그림처럼 A, B 각도의 장애물을 있는지 측정해서 둘 중 안전한 방향을 찾아 진행 방향을 선택하는 방식입니다.

void loop() {
  delay(50);  

  //거리측정
  int distance = sonar.ping_cm();
  
  //장애물 감지
  if(distance>0 && distance<15){
    주행정지
      angle=135;
        servo.write(angle);
        delay(500);
        int distanceA = sonar.ping_cm();
        angle=45;
        servo.write(angle);
        delay(500);
        int distanceB = sonar.ping_cm();
        
    if(distanceA<distanceB){
         B방향으로 피하기 주행;  
    }
    else{
         A방향으로 피하기 주행;
    }
  }
  전방주행; 
}

위 소스는 0.05초 간격으로 전방 장애물을 감지하다가 장애물이 발견되면 if문으로 들어가서 먼저 주행이 정지되고 그다음 135도와 45도의 방향으로 장애물을 감지합니다. 그리고 둘 중 거리값이 긴 쪽으로 장애물을 피하는 동작을 만들 수 있습니다.

2) 고정 초음파센서 장애물 감지



고정 형태로 한개의 초음파센서로 전방 장애물만 감지합니다. 전방 장애물을 감지 할 때 장애물을 피하기 동작 패턴을 만들기가 애매합니다. 그냥 랜덤 방향으로 할지 아니면 무조건 우방향으로 피할지는 여러분의 마음에 달렸습니다.

void loop() {
  delay(50);  

  //거리측정
  int distance = sonar.ping_cm();
  
  //장애물 감지
  if(distance<15){
    장애물 감지 방향선택;
  }
  전방주행; 
}

코딩은 가장 단순합니다. if문으로 전방 장애물이 발견 되면 그냥 후진, 좌/우 방향으로 진로 변경 할지는 여러분들의 선택으로 하나의 방향을 만들어 주면 됩니다.

3) 양쪽 두개의 고정 초음파 센서 장애물 감지



두개의 고정된 초음파 센서로 장애물을 감지할 경우 A, B 두지점으로 좌/우 방향 위치의 장애물을 감지하게 됩니다. 이 경우는 위에서 전방 장애물을 감지 되었을 때 주행을 정지 한 상태에서 좌/우 방향 위치의 장애물을 감지 했던 코딩과 유사 합니다. 차이점은 주행 중에 실시간으로 좌/우 방향의 장애물을 감지 한다는 점만 차이가 있습니다.

void loop() {
  //거리측정
  delay(50);  
  int distanceA = sonar[0].ping_cm();  
  delay(50);  
  int distanceB = sonar[1].ping_cm();
    
  //장애물 감지
  if(distanceA<15 || distanceB<15){
    if(distanceA<distanceB){
         B방향으로 피하기 주행;  
    }
    else{
         A방향으로 피하기 주행;
    }
  }
  전방주행; 
}

이렇게 두 지점의 거리를 측정 한 뒤에 if문에서 조건식을 체크하는데 '||'로 두 조건식 중 하나라도 만족하면 참인 조건문으로 체크하게 됩니다. 둘중 하나라도 만족하면 진로 방향에 장애물이 있다고 간주하는 것이죠. 즉, A or B의 거리가 15cm 이하이면 참이되어 장애물을 감지된 걸로 간주 합니다. 여기서, 두 지점을 비교하여 A지점이 거리가 더 짧으면 B방향으로 주행하게 만들고 A지점의 거리가 더 길면 A방향으로 주행하도록 만들 수 있습니다.

이외에도 장애물 감지 방식은 여러분들이 상상하는 방식에 따라서 다양하게 표현이 가능합니다. 순간 떠오른 생각들을 몇가지 정리한 것이 위의 장애물 감지 표현입니다. 여러분들도 이 방식 말고 다른 방식으로 장애물을 감지할 수 있는 방법을 찾아보셨으면 합니다. 그래야 상상코딩 능력을 키울 수 있습니다.

마무리


오늘은 초음파센서를 이용하여 장애물을 잠지하는 방법들을 살펴 보았습니다. 초음파센서를 어떤 형태로 배치하느냐에 따라서 감지하는 방식이 달라집니다. 어떤 방식이 정도라도 말 할 수 없으며 여러분들이 원하는 방식으로 표현하시는게 바로 정도입니다. 그리고, 위에서 열거한 방식으로 구지 안하셔도 됩니다. 여러분들의 상상력으로 감지 방식을 만들면 됩니다.

장애물 감지를 위해 실험에 사용한 초음파센서를 이용하셔도 되고 다른 거리측정 센서를 이용하셔도 됩니다. 아니면 전혀 다른 방식으로 주변을 감지할 수 있는 부품을 사용하여 표현하셔도 됩니다. 저는 가장 쉽게 접근할 수 있는 방식이 초음파센서라서 초음파센서를 사용한 것 뿐입니다. 여러분들이 다른 방식으로 하기 어려우시다면 1200원 짜리 초음파센서를 이용하여 실험하셔도 됩니다. 초음파센서를 어떻게 배치하고 사용할지는 여러분의 상상력에 달려 있음으로 한번 상상의 나래를 펼쳐보세요.

댓글()

[아두이노] 초음파센서 아두이노 RC카 자율주행 준비 입문자용

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

[아두이노] 초음파센서 아두이노 RC카 자율주행 준비 입문자용



아두이노 2륜 RC카를 가지고 초음파센서와 결합하여 장애물을 감지하면서 자율주행을 할 수 있도록 하는 간단한 실험을 할 예정입니다. 오늘 Post는 초음파센서를 이용한 자율주행 RC카를 만들기 위한 몸체 조립과 조립에 대한 테스트 동작을 통해 아무 문제가 없는지 확인하는 단계의 내용으로 구성되어 있는데 천천히 배워보도록 하죠.

진짜로, 정교한 자율주행 RC카를 만든다면 영상처리 부분까지 하셔야 합니다. 입문자분들이 바로 영상처리까지 하기는 무리가 따릅니다. 저도 영상처리 부분은 어렵고 실시간 영상을 가지고 주변환경 영상의 에지를 추출하여 그 값을 기반으로 주행 처리를 해야하는데 대충 코딩 방향은 아는데 실시간 영상에서 에지 검출이 좀 힘들어서 설명드리기가 어렵네요. 에지 검출이 영상처리책 보면 대표적으로 canny, sobel 에지 검출 알고리즘이 있습니다. 이 알고리즘으로 정지 영상에서 에지를 추출하는 것은 쉽지만 실시간 영상에서는 저도 알고리즘 로직이 잘 감이 안와서 설명을 못 드리겠네요. opencv로 얻은 영상에서 에지를 검출하고 그 영상을 기반으로 주행을 시키는 것 같은데 고급 자율주행은 사실 깊게 이쪽 분야로 공부를 안해서 아쉽게 실험을 못해 봤습니다. 나중에 라즈베리파이로 공부할 때 한번 도전은 해보고 싶긴 하네요.

사설은 그만하고 오늘 post의 주제는 지난 시간에 만든 아두이노 RC카 정면에 초음파센서를 부착하는 조립하는 단계입니다. 초음파센서에 서보모터를 통해 0~180도 사이각의 회전을 시킬 수 있는 형태로 조립을 하게 되는데 사실 뼈대가 부품가격과 거의 비슷하기 때문에 그냥 테이프로 부착시켰습니다. 테이프로 부착하다보니 정교한 측정이 되지 않지만 그래도 근사값 측정을 통해서 원하는 동작을 충분히 실험할 수 있기 때문에 아무런 문제는 없습니다. 여러분들은 뼈대를 구매하거나 3D 프린트로 직접 만들시면 좋겠죠. 저처럼 테이프로 고정시키는 실험은 보기 흉하니깐 뼈대를 사셔서 이쁘게 만들어 보세요.


이제 본격적으로 아두이노 RC카 자율주행을 도전 할까요.

1. 아두이노 RC카 준비



사전학습 post로 가셔서 아두이노 RC카에 대한 학습을 먼저 하셔야 합니다. 대충 사전학습에서 RC카 조립을 끝낸 상태면 이렇게 완성되어 있겠죠.


조립이 완성된 상태에서 다음 준비 부품이 필요합니다.

2. 아두이노 RC카에 추가 부품 조립


  • 준비물 : 아두이노 RC카 1대, 초음파센서 1개, ServoMotor 1개


위 사진에서는 뼈대가 있으면 좋은데 간단히 실험을 하기 위해서 그냥 테이프로 붙였습니다. 아두이노 RC카 정면에 Servo Motor와 초음파 센서를 정면을 바라보도록 배치하시면 됩니다. 혹시 뼈대를 개인적으로 구매하신다면 안정적으로 배치 할 수 있겠죠. 저는 테이프로 붙여서 각도도 안맞고 약간 아래로 쳐져 있어서 좀 불편하게 실험 했네요.


위에서 바라보는 모습입니다. 보시면 A은 초음파센서핀으로 연결합니다. 초음파센서 핀은 A4(Trig), A5(Echo)핀입니다. 그리고 B는 Servo Motor 핀을 연결합니다. 바깥쪽부터 (-,+,sig) 이렇게 순서대로 있는데 3핀을 순서대로 연결하시면 됩니다. 제가 실험한 모터쉴드는 2개의 Serovo Motor 핀을 제공하는데 상단 첫번째 줄은 아두이노 디지털핀 10번이고 두번째 줄은 9번핀이네요. 10번핀을 사용하였습니다.

좀 더 자세히 아래 사진의 L293D Motor Shield의 모습을 보시고 연결하세요.


해당 위치에 핀을 꼽아주고 아두이노 RC카 정면에 Servo Motor와 초음파 센서를 부착하시면 조립은 완성입니다.

2. 간단히 장애물 감지센서 시범 회전 코딩



Servo Motor

#include <Servo.h> 
  • Servo servo : 서보모터 객체변수 선언
  • servo.attach(servoPin) : 서보모터 시작하는데 사용되는 핀은 servoPin(10) 임
  • servo.write(angle) : angle 값으로 서보모터를 회전 시킨다.

NewPing

#include <NewPing.h>
  • NewPing sonar(TrigPin, EchoPin, MaxDistance) : TrigPin과 EchoPin과 최대제한거리(MaxDistance)의 값을 선언합니다.
  • sonar.ping_cm() : 센서 거리를 'cm'로 계산된 값을 출력한다.

1) Servo Motor 회전

Servo Motor가 정상적으로 회전이 되는지 테스트 코딩을 해야 겠지요.

Servo servo;
const int servoPin = 10;
int state = 1;
int angle = 90;

이렇게 우선 서보모터 객체변수을 하나 만들고 서보모터를 제어할 아두이노 디지털 핀 10을 선언하고 초기 각도(angle) 값을 90도로 표현 했습니다. 그리고 state은 초기값이 1인데 이 값은 1이면 1씩 증가 -1이면 1씩 감소하는 변수로 사용하기 위해 만든 변수입니다.

void setup() {
  servo.attach(servoPin);
  servo.write(angle);
  delay(1000);
}

setup()함수에서 초기화 작업을 하는데 서보모터를 시작하고 바로 servo.write()함수를 사용하여 angle(90)각도로 회전합니다. 딜레이 시간은 1초를 줬네요. 초기값이라서 원하는 시간값을 여러분들이 마음대로 결정하세요. 서보모터를 회전시킬 만큼의 딜레이시간을 줘야하는데 1초은 꽤 긴시간이고 더 짧게 딜레이시간을 주고 90도까지 회전 시켜도 됩니다. loop()함수로 진입하기 전에 충분히 대기시간을 주기 위해서 1초정도 딜레이를 줬네요.

void loop() {
  //회전
  servo.write(angle);
  delay(50);  

  //회전 각도
  if (angle == 180) state = -1;    
  else if (angle == 0) state = 1; 
  angle += state;
}

loop()함수는 0.05초 간격으로 서보모터를 angle(각)으로 회전시키게 됩니다. 어떻게 회전될 까요 회전 각도 로직을 살펴보시기 바랍니다.

  if (angle == 180) state = -1;    
  else if (angle == 0) state = 1; 
  angle += state;

보시면 angle이 180도면 state은 -1이고 angle이 0도이면 state 값은 1이다. 이 의미를 이해하기 위해서는 아래 문장을 보시면 쉽게 이해가 되실 꺼에요.

angle = angle + state;

이 문장은 0.05초 간격으로 loop()함수가 무한 반복하는데 기존에 angle값에다가 state을 계속 더해주는 문장입니다. 여기서, state가 1이면 1씩 증가하고 state가 -1이면 1씩 감소하게 됩니다. 의미를 잘 이해해 주세요. 이렇게 되면 초기값이 state가 1이기 때문에 초기각도 angle이 90도니깐 1도씩 증가하게 됩니다.

서보모터는 0~180도까지 제어가 가능합니다 그러면 1씩 증가하더라도 180도를 넘을 수 없게 됩니다 180도가 되면 state값을 -1로 변경하면 1도씩 감소하게 되고 역방향으로 회전하게 됩니다. 그러면, 다시 역방향으로 1도씩 감소하는데 angle은 0이하로 떨어질 수 없습니다. 이때 state 값을 1로 변경하고 다시 정방향으로 1도씩 증가하게 됩니다. 이렇게 표현하면 0~180도 사이를 1도씩 왔다 갔다 회전하게 됩니다.

loop()함수 로직의 동작은 0.05초 간격으로 0~180도 사이를 무한 반복 회전을 하게 됩니다. 이 움직임을 보시면 초음파레이더가 생각 나시는 분이 있을 지 모르겠네요. 초음파레이더의 기본 동작 코딩입니다. 이 코딩으로 초음파레이더를 만들 수 있습니다. 현재 우리가 초음파레이더를 만드는 것이 목적이 아니기 때문에 서보모터 회전에 대한 테스트 동작으로만 이해해 주셨으면 합니다.

2) 초음파센서 거리 측정


이제는 서보모터가 회전할 때마다 초음파센서로 거리를 측정해야 겠죠.

NewPing sonar(A4, A5, 100);

trig(A4), Echo(A5), 제한거리1미터(100)으로 초음파센서 객체변수를 만듭니다.

  int distance = sonar.ping_cm();

distance 변수에 초음파센서에서 측정한 거리(cm)값이 저장됩니다. 이렇게 해서 거리 측정이 끝났네요.

그러면 이 코딩은 전체소스 안에서 어느 위치에 삽입하는 것이 좋을까요. 회전 시마다 초음파센서로 측정한다고 했죠. 서보모터는 어떻게 회전한다고 했죠. 0.05초 간격으로 회전이 이뤄집니다. 그러면, 0.05초 간격으로 회전할 때 초음파센서로 측정하면 되겠죠. 즉, 서보모터를 angle(각)으로 회전시킨 후 초음파센서로 거리를 측정하게 코딩하면 간단히 해결 됩니다.

void loop() {
  //회전
  servo.write(angle);
  delay(50);  
    
  int distance = sonar.ping_cm();
    
  //회전 각도
  if (angle == 180) state = -1;    
  else if (angle == 0) state = 1; 
  angle += state;
}

이렇게 하면, 0.05초 간격으로 서보모터를 회전시키고 distance 변수에 회전 시킨 각도에서 거리측정한 값이 저장됩니다.

그러면, 이 값을 시리얼모니터로 정상적으로 출력되는지 Serial 함수로 표현하면 되겠죠.


  • Serial.begin(9600) : 시리얼통신 시작
  • Serial.print(distance) : 시리얼모니터로 출력

3) 종합 코딩


좀 더 깔끔하게 각도와 거리값을 동시에 시리얼모니터로 출력시켜서 확인할 수 있도록 소스를 수정하였습니다. Serial함수는 너무 많이 설명을 드렸기 때문에 구지 설명을 안드려도 아실꺼라 믿고 아래 변경된 부분만 잘 확인해 주세요.

참고로, 완성된 소스는 초음파레이더 소스와 동일합니다. 이 소스로 processing으로 시각화 하면 초음파레이더가 됩니다. 초음파레이더로 보시지 마시고 그냥 앞에 장애물을 감지하는 센서로 보셨으면 합니다. 서보모터의 회전이 정상적으로 이뤄지고 거리측정이 정상적으로 이루어지는지 테스트하는 용도로 코딩을 바라 보시기 바랍니다.

[소스]

#include <Servo.h> 
#include <NewPing.h>

const int TRIG = A4;
const int ECHO = A5;
const int MAX_DISTANCE = 100;
NewPing sonar(TRIG, ECHO, MAX_DISTANCE);

Servo servo;
const int servoPin = 10;
int state = 1;
int angle = 90;

void setup() {
  Serial.begin(9600);
  servo.attach(servoPin);
  servo.write(angle);
  delay(1000);
}

void loop() {
  //회전
  servo.write(angle);
  delay(50);  

  //거리측정
  int distance = sonar.ping_cm();
  Serial.print(angle);        
  Serial.print(" : ");        
  Serial.print(distance);    
  Serial.println(" cm");         

  //회전 각도
  if (angle == 180) state = -1;    
  else if (angle == 0) state = 1; 
  angle += state;
}

newPing 라이브러리를 혹시 안깔고 코딩하신분들이 있을 것 같아서

라이브러리 매너저 창에서 "newPing"이라고 검색하시면 자동으로 검색되고 해당 라이브러리를 설치하시면 됩니다.


3. 결과


촬영이 좀 깔끔하게 되지 못했네요. 폰으로 찍어서 하다 보니깐 공간적 제약도 따르고 해서 좀 보기 불편하시더라도 대충 어떤식으로 Servo Motor가 회전 되고 초음파센서를 통해 거리가 측정되는지 영상으로 살펴보시기 바랍니다. 참고로 위쪽 상단는 모니터가 있어서 거리값이 10~11정도로 측정됩니다. 오류값이 아니라 모니터와의 거리 때문에 나온 값이니깐 감안하시고 보세요.


마무리


오늘은 아두이노 RC카가 준비된 상태에서 두개의 부품 Servo Motor와 초음파센서를 조립에 대해 간단히 살펴보았습니다. 그리고 정상적으로 동작하는지 실험 코딩으로 테스트까지 해보았습니다. 이렇게 해서 초음파센서를 통해 아두이노 RC카는 정면의 장애물 감지까지 할 수 있게 되었습니다. 다음 단계는주행 도중에 장애물이 감지된 정보에 대해 어떻게 반응 할 것인지가 남아 있습니다.

사실 위에서 정상 작동하는지 테스트한 코딩을 이해하셨다면 조금만 상상을 더하면 장애물을 피해 자율주행을 시키는 코딩은 어렵지 않게 하실 수 있을 겁니다. 이미 이 post로 아두이노 RC카 자율주행은 끝난 거나 다름 없습니다. 그런데, 이걸로 어떻게 자율주행이 되냐고 생각하시는 분들도 많을 꺼에요. 잘 연상이 안되실 꺼에요. 상상을 많이하고 자기 자신이 RC카라고 상상하면서 내 이마에 서보모터가 부착되어 회전하고 있고 회전하면서 초음파센서로 거리를 측정하고 있다고 상상하면서 한번 제자리에서 걸어 다녀보세요. 전방에 벽이 나타나면 여러분들은 걷다가 멈추게 되겠죠. 그리고, 그 벽을 피해서 어떤 행동을 하겠죠. 벽 앞에서 다시 뒤로 되돌아 간다거나 오른쪽 길이 있으면 오른쪽으로 걸음을 옮기게 되겠죠. 이렇게 어떤 행동을 취하게 됩니다. 그 행동을 코딩화 하면 그 코딩이 아두이노 RC카의 자율주행 코딩이 됩니다. 즉, 어떤 상황이 되면 그 상황을 피하기 위해서 어떤 행동을 취하고 그 행동이 자율 주행 명령 코드가 되어 실제로 아두이노 RC카가 자율주행을 하게 됩니다. 코딩은 모르더라도 대충 어떤 느낌이신지 아시겠지요.

한번 자기자신이 아두이노 RC카라고 상상하고 제자리에서 주변을 걸어다녀보시면서 상황을 만들고 그 상황을 기록했다가 상상 코딩을 해보셨으면 합니다.


댓글()

[아두이노] newPing 라이브러리로 초음파센서 제어

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

[아두이노] newPing 라이브러리로 초음파센서 제어



이제까지 초음파센서를 이용하여 거리를 구하기 위해서 직접 공식에 대입하여 코딩을 했었습니다. 직접 코딩하는 것이 불편하신 분들을 위해 newPing이라는 라이브러리를 소개할까 합니다. newPing이라는 라이브러리를 통해서 초음파센서를 쉽게 제어하여 원하는 거리측정 값을 얻을 수 있습니다. newPing 라이브러리를 이야기하기전에 우선 초음파센서 거리측정에 대해서 복습해야겠죠.

초음파센서 거리를 구하는 공식은 다음과 같습니다.

duration = 초음파 센서를 통해 읽은 거리 시간값
distance = ((float)(340 * duration) / 10000) / 2;

위 공식은 복습차원으로 봐주세요. duration을 구하는 코딩 로직은 참고 자료 post를 한번 읽고 와주세요.


여기까지 복습이 완료 된 상태라면 newPing 라이브러리로 실제 실험을 해봅시다.

1. 초음파센서 회로도



공개회로도는 3핀 초음파센서로 직접 측정 로직을 코딩하고 직접 측정한 시간값을 거리를 구하는 공식에 대입하여 구한 예제입니다. 실제 실험에서는 4pin으로 구성된 초음파 센서를 실험하기 때문에 회로도를 표현하면 아래 회로도와 같습니다.


2. newPing 라이브러리 설치


라이브러리 매너저 창에서 "newPing"이라고 검색하시면 자동으로 검색되고 해당 라이브러리를 설치하시면 됩니다.


3. 코딩



NewPing

#include <NewPing.h>
  • NewPing sonar(TrigPin, EchoPin, MaxDistance) : TrigPin과 EchoPin과 최대제한거리(MaxDistance)의 값을 선언합니다.
  • sonar.ping_cm() : 센서 거리를 'cm'로 계산된 값을 출력한다.
  • 다수의 초음파센서 사용 :
NewPing sonar[센서수] = { 
  NewPing(trigPin1, echoPin1, 최대제한거리1), 
  NewPing(trigPin2, echoPin2, 최대제한거리2), 
    ...
};
  • 다수의 초음파센서 접근 :
  • sonar[0].ping_cm() : 0번째 sonar에서 측정된 거리를 'cm'로 계산된 값을 출력한다.

그외 함수는 newPing 라이브러리가 링크된 아두이노 공식 사이트에 가시면 함수들에 대해 자세히 설명되어 있으니깐 가셔서 한번 읽어 주시기 바랍니다.

[소스] : NewPingExample 소스(출처: newPing 라이브러리 예제)

#include <NewPing.h>

//sonar(TrigPin, EchoPin, MaxDistance);
NewPing sonar(2, 3, 200);

void setup() {
  Serial.begin(9600);
}

void loop() {
  delay(50);          
  Serial.print("Ping : ");
  Serial.print(sonar.ping_cm());
  Serial.println("cm");
}

sonar.ping_cm() 함수를 통해서 초음파 센서의 거리를 'cm'로 출력된 값을 시리얼모니터로 출력하는 코딩입니다. 딱 한줄입니다. sonar.ping_cm() 함수 이 한줄의 명령을 실험하는 내용입니다. 그걸 시리얼모니터에 출력하기 위해서 3줄의 시리얼 출력문을 코딩했을 뿐 실제 초음파센서를 제어하는 명령은 딱 한줄입니다.

void loop() {
 delay(50);          
 sonar.ping_cm();
}

이렇게 0.05초 간격으로 초음파센서는 거리측정 값을 'cm'을 반환합니다. 어렵지 않죠.

라이브러리를 이용하면 이 함수 한개로 복잡하게 코딩을 생각하지 않고 거리를 'cm'로 만들 수 있지만 직접 거리를 계산하는 공식 코딩은 단순하기 때문에 구지 라이브러리를 사용 안해도 됩니다.

그런데, 왜! 라이브러리를 사용하는지 궁금하실 꺼에요. 거리 측정하는 거리 공식도 별로 어렵지 않은데 그냥 코딩하면 되지 라이브러리를 사용할 필요가 있냐고 생각하실 꺼에요. 그 이유는 초음파 센서를 한개 사용할 때는 상관 없는데 다중 초음파센서를 사용할 때 newPing 라이브러리 사용하면 효율적으로 코딩을 할 수 있습니다. 표현도 배열로 간단히 하나의 묶음으로 표현 할 수 있고, 여러개를 측정하려면 시간 관련한 문제도 고려해서 코딩해야 하는데 라이브러리를 사용하면 좀 더 정교하게 코딩이 가능하기 때문에 사용합니다. 위에 링크 된 아두이노 공식 홈페이지에서 소개하는 newPing 라이브러리 관련 내용을 보시면 15개 초음파센서를 한번에 제어하는 예제가 있는데 가셔서 살펴 봐주세요. 물론 라이브러리를 설치하면 아두이노 IDE에 예제로 나와 있어 설치하고 나서 예제를 열어보시고 어떻게 코딩되어 있는지 살펴보셔도 됩니다.

3. 결과


4. 2개 초음파 센서 사용


이제 2개의 초음파를 한번에 측정하여 그 결과를 출력해 보는 실험을 해보겠습니다.

1) 2개 초음파 센서 회로도


  • 준비물 : 초음파센서 2개, 아두이노우노
  • 내용 : 왼쪽 초음파센서는 tragPin 2, echoPin 3 에 연결하고, 오른쪽 초음파센서는 tragPin 4, echoPin 5 에 연결하시오.


2) 코딩


배열로 초음파센서 객체를 선언합니다.

NewPing sonar[2] = { 
  NewPing(2, 3, 200), 
  NewPing(4, 5, 200), 
};

이렇게 선언하시면 초음파센서 측정은 sonar[0].ping_cm(), sonar[1].ping_cm() 함수로 해서 2개의 초음파 센서의 거리를 측정하여 'cm'로 출력하게 됩니다. 초음파 센서들 간의 시간 딜레이는 0.05 초로 간격을 두어 측정하게 됩니다. 너무 빠르게 두개의 초음파센서의 값이 시리얼모니터로 출력되기 때문에 딜레이를 loop()함수 안에 마지막 라인에 0.5초의 딜레이를 줌으로서 좀 천천히 2개의 초음파센서 값이 시리얼모니터로 출력되게 코딩했네요.

코딩은 따로 설명할 필요 없이 방금 앞에서 코딩한 소스에서 한번 더 초음파센서 sonar[위치].ping_cm()함수로 출력하는 명령만 추가한 것 뿐이니 구지 설명하지 않겠습니다.

[소스]

#include <NewPing.h>

//sonar(TrigPin, EchoPin, MaxDistance);
NewPing sonar[2] = { 
  NewPing(2, 3, 200), 
  NewPing(4, 5, 200), 
};

void setup() {
  Serial.begin(9600);
}

void loop() {
  delay(50);          
  Serial.print("A Ping : ");
  Serial.print(sonar[0].ping_cm());
  Serial.println("cm");
  
  delay(50);          
  Serial.print("B Ping : ");
  Serial.print(sonar[1].ping_cm());
  Serial.println("cm");

  delay(500);
}

3) 결과



위 사진을 보시면 A초음파센서와 B초음파센서로 나뉩니다. 여기서, B초음파센서는 Servo Motor에 테이프로 감겨 있고 Servo Motor 같이 아두이노 RC카에 테이프로 부착되어 있어서 어쩔 수 없이 RC카에 부착된 B초음파센서랑 A초음파센서를 실험하다 보니깐 보시는 것처럼 좀 불편하게 실험 되었습니다. 감안하시고 동영상을 보시기 바랍니다.


마무리


오늘 post는 초음파센서를 직접 코딩하는 방법도 있지만 이렇게 라이브러리를 이용하여 쉽게 코딩할 수 있는 것을 보여드리기 위해서 다뤘습니다. 그리고, 왜! 갑자기 초음파센서를 이 시점에 다시 post를 했냐면 아두이노 RC카에서 거리측정을 통해 장애물을 감지하는데 사용하기 위해서 사전 학습으로 거론하게 되었네요. 직접적으로, 측정 로직을 코딩하는 것도 좋지만 새로운 것을 소개하는 것이 좋을 것 같아서 초음파센서에 대한 post를 하면서 newPing에 대해 이야기 하면 좋을 것 같아 이렇게 이야기를 하게 되었네요.

오늘 이야기한 post를 보기전 사전학습으로 직접 거리를 측정하는 방법에 대한 링크된 post 글을 찾아가셔서 복습을 한 뒤에 newPing을 사용해 보셨으면 합니다.


댓글()

[아두이노] 2륜 RC카 원리를 가상시뮬레이터로 실험

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

[아두이노] 2륜 RC카 원리를 가상시뮬레이터로 실험



지난 시간에 기본 아두이노 2륜 RC카 조정에 대해서 마무리 했습니다. post로만 아두이노 RC카 설명을 끝내기가 아쉬워서 가상시뮬레이터에서 동일하게 표현하고 코딩도 유사하게 코딩해서 간접적으로나마 체험 할 수 있게 하면 좋을 것 같아서 간단히 가상시뮬레이터에서 회로도를 만들어 보았습니다.

구체적으로 어떻게 표현했는지 설명 하겠습니다.

1. 아두이노 2륜 RC카 회도로


  • 준비물 : L293D 칩 1개, DC Motor 2개, 외부전원, 아두이노우노
  • 내용 : 아두이노 위에 L293D 칩 pin 들을 아두이노우노에 원하는 위치에 연결한다.
  • 참고 : [아두이노] L293D + DC MOTOR 제어

회로도가 복잡해 보일 꺼에요. 회로도가 이해가 안가신다면 위에 링크 걸린 참고 자료에 가셔서 복습하시면 됩니다.

참고로, [아두이노] 2륜 RC카 Bluetooth를 통해 스마트폰(무선) 조정하기 post의 동작과 동일합니다. 실제로 만들 경우에 동일한 결과가 나오겠지요.

그리고, 이 회로도에서 Bluetooth은 0,1 핀을 사용한다는 가정하에서 진행됩니다. 저번에 설명했듯이 아두이노 내부 시리얼통신 핀을 사용할 경우는 SoftwareSerial 라이브러리가 필요 없습니다. 그냥 상상으로 0,1번핀에 Bluetooth가 연결되었다고 상상하신 후 보시기 바랍니다.

Bluetooth Tx = arduino Pin0
Bluetooth Rx = arduino Pin1

3. Motor 라이브러리 만들기



원래는 Adafruit-Motor-Shield-library 를 인용해서 코딩해야 하는데 해당 부분만 편집하면 좀 문제가 생길 수 있고 해서 지난시간에 이 라이브러리로 실험했던 느낌을 계속 유지하기 위해서 라이브러리에 사용한 함수명만 그대로 인용해서 새롭게 Motor 라이브러리를 만들어 보았습니다.


  • AF_DCMotor motor(3) : M3핀을 DC Motor 제어용으로 사용.
  • motor.setSpeed(200) : 모터 속도
  • motor.run(FORWARD) : FORWARD, BACKWARD, RELEASE 회전 명령

기본 구조가 이렇게 위처럼 되어 있는데 혼동을 피하고 같은 느낌을 계속 유지하기 위해서 위 표현을 그대로 인용 합니다.

#define FORWARD  1
#define BACKWARD 2
#define RELEASE  3

class DCMotor{
 public:
   DCMotor(uint8_t pinA, uint8_t pinB, uint8_t pinC);
   void run(uint8_t);
   void setSpeed(uint8_t);
   private:
   uint8_t motorA, motorB,speedPin;
};

위 처럼 우선 생성자 DCMotor() 함수를 표현 했습니다. 어떤 핀을 Motor로 제어할지 Motor 객체변수를 선언과 동시에 지정해주기 위해서 입니다. 그리고, run(), setSpeed() 함수명을 그대로 인용했네요.

함수들의 로직 코딩을 볼까요.

DCMotor::DCMotor(uint8_t pinA, uint8_t pinB, uint8_t pinC) {
  motorA = pinA;
  motorB = pinB;
  speedPin = pinC;
  pinMode(motorA,OUTPUT);
  pinMode(motorB,OUTPUT);
  pinMode(speedPin,OUTPUT);
}

DCMotor()함수는 private 형으로 선언한 클래스 내부 변수들에 대한 초기화 작업을 수행합니다. 각 pin의 사용 모드도 여기서 초기화 합니다. 사실 클래스 형태로 표현할려면 이런 코딩이 아니라 아두이노 주소번지로 직접 접근하여 좀 더 하드웨어적 코딩을 해야하는데 복잡해 보일 것 같아서 아두이노 함수로 간단히 클래스를 표현했네요. 느낌만 표현한 클래스 입니다. 진짜 만든다면 이렇게 하면 안되고요. 좀 더 하드웨어적 코딩을 해야 합니다. 우리는 즐기기 위해서 하는 것이기 때문에 쉽게 코딩하는게 좋겠죠.

void DCMotor::run(uint8_t key) {
  switch (key) {
  case 1:
    digitalWrite(motorA,HIGH); //전진
    digitalWrite(motorB,LOW);
    break;
  case 2:
    digitalWrite(motorA,LOW); //후진
    digitalWrite(motorB,HIGH);
    break;
  case 3:
    digitalWrite(motorA,LOW); //정지
    digitalWrite(motorB,LOW);
    break;
  default:
    return;
  }
}

run()함수는 key 값은 FORWARD, BACKWARD, RELEASE 값을 말합니다. define으로 정의한 1, 2, 3값에 해당되는 switch()함수를 통해서 해당된 pin을 digitalWrite()함수로 High or Low로 결정하게 됩니다. 이 코딩은 이미 지난 Post L293D + DC MOTOR 제어 (아두이노) 에서 소개 했습니다. 혹시 잘 모르시겠다면 해당 post에 가셔서 한번 읽고 오시기 바랍니다.

void DCMotor::setSpeed(uint8_t speed) {
   analogWrite(speedPin, speed);
}

setSpeed()함수는 Enable Pin으로 값을 출력하는데 PWM Pin을 사용하여 0~255 사이의 아날로그 신호를 출력합니다. speed의 값을 analogWrite()함수로 아날로그 신호를 출력하면 되기 때문에 딱 한줄 명령으로 표현 됩니다.

이렇게 해서 DCMotor 클래스를 간단히 표현 했습니다.

3. 코딩


만든 DC Motor 클래스

  • DCMotor motor1(7,6,5);) : In1, In2, EnablePin
  • motor.setSpeed(200) : 모터 속도
  • motor.run(FORWARD) : FORWARD, BACKWARD, RELEASE 회전 명령

[소스]

#define FORWARD  1
#define BACKWARD 2
#define RELEASE  3

class DCMotor{
 public:
   DCMotor(uint8_t pinA, uint8_t pinB, uint8_t pinC);
   void run(uint8_t);
   void setSpeed(uint8_t);
   private:
   uint8_t motorA, motorB,speedPin;
};

DCMotor::DCMotor(uint8_t pinA, uint8_t pinB, uint8_t pinC) {
  motorA = pinA;
  motorB = pinB;
  speedPin = pinC;
  pinMode(motorA,OUTPUT);
  pinMode(motorB,OUTPUT);
  pinMode(speedPin,OUTPUT);
}
void DCMotor::run(uint8_t key) {
  switch (key) {
  case 1:
    digitalWrite(motorA,HIGH);
    digitalWrite(motorB,LOW);
    break;
  case 2:
    digitalWrite(motorA,LOW);
    digitalWrite(motorB,HIGH);
    break;
  case 3:
    digitalWrite(motorA,LOW);
    digitalWrite(motorB,LOW);
    break;
  default:
    return;
  }
}

void DCMotor::setSpeed(uint8_t speed) {
   analogWrite(speedPin, speed);
}
                 
DCMotor motor1(7,6,5); //L293D Motor Pin과 Enable Pin
DCMotor motor2(9,8,3);

int speed = 200;
                 
void setup() {
  Serial.begin(9600);   
  motor1.setSpeed(speed);
  motor2.setSpeed(speed);
  motor1.run(RELEASE);
  motor2.run(RELEASE); 
}
 
void loop() {
  if (Serial.available()){
    char ch = Serial.read();

    switch(ch){
      case 'w':
            motor1.run(FORWARD);
            motor2.run(FORWARD);
            break;
      case 's':
            motor1.run(BACKWARD);
            motor2.run(BACKWARD);
            break;
      case 'a':
            motor1.run(BACKWARD);
            motor2.run(FORWARD);
            break;
      case 'd':
            motor1.run(FORWARD);
            motor2.run(BACKWARD);
            break;
      case 'z':
            motor1.run(RELEASE);
            motor2.run(RELEASE);
            break;
      case 'm':
            speed+=10;
            if(speed>=250) speed=250;
            motor1.setSpeed(speed);
            motor2.setSpeed(speed);
            break;
      case 'n':
            speed-=10;
            if(speed<=0) speed=0;
            motor1.setSpeed(speed);
            motor2.setSpeed(speed);
            break;
    }         
  }
}

만든 Motor 클래스만 추가되고 setup(), loop()함수는 실제 제작했던 Post [아두이노] 2륜 RC카 Bluetooth를 통해 스마트폰(무선) 조정하기 와 동일한 소스 입니다.

이렇게 해서 가상시뮬레이터로 회로도를 만들고 코딩도 동일하게 코딩해서 실험할 수 있도록 세팅이 끝났네요.

위 소스에서 실제로 L293D Motor Shield로 제어한다면 해당 라이브러리를 다운 받은 후 객체 선언부분만 형식에 맞춰서 하신 후 setup(), loop()함수는 수정 없이 그대로 사용하면 실제로 스마트폰으로 조정이 가능 합니다.

5. 결과


가상시뮬레이터를 싱행 시킨 후 위 그림처럼 시리얼모니터에서 회전 명령 키값을 전송하면 됩니다. 스마트폰의 역활을 대신하는 것이죠. 0,1번 핀에 Bluetooth Pin을 연결하면 바로 스마트폰으로 제어가 가능합니다.

전진(w)/후진(s), 좌(a)/우회전(d), 정지(z), 속도 증가(m)/감소(n) 의 명령으로 세팅 되어 있고 가상시뮬레이터가 스마트폰으로 상상하고 키값을 입력하여 전송하면 거기에 맞게 가상시뮬레이터에 DC기어모터가 회전을 하게 됩니다.

아래는 테스트 결과입니다.


위 영상이 제대로 식별이 안되신다면 위에 공개회로도를 링크 걸어 놓았어요. 가셔서 실제로 실행 시켜보셔서 확인하시면 됩니다. 시뮬레이더 실행 키 누르고 코딩창 열고 코딩창 하단에 시리얼모니터 아이콘을 눌러서 창을 개방시키고 조정 키값을 입력하시면 회로도의 DC기어모터가 조정 키값에 따른 회전명령을 수행합니다.

마무리


실제로 아두이노 RC카 부품을 구매해서 직접 제작하지 않더라도 간접적으로 이렇게 가상시뮬레이터에서 실험을 할 수 있습니다. 실제로 주행하는 모습은 볼 수 없지만 DC Motor 회전만으로도 간접적으로 회전의 원리를 체험할 수 있습니다.

실험을 하실 때 Post [아두이노] 아두이노 2륜 RC카 주행 패턴 실험 에서 그림으로 주행 패턴을 설명했는데 그 그림을 보시고 가상시뮬레이터의 DC기어모터 회전을 합쳐진 이미지를 머리속에서 상상하시면서 오늘 post의 내용을 보시면 대충 상상속에서 주행하는 모습이 이미지로 그려질 거라 생각합니다.

이렇게 해서 아두이노 RC카를 만들고 Bluetooth로 무선 조정하고 마무리로 가상시뮬레이터로 표현까지 해서 꽤 긴 Post를 마무리 합니다. 사실 간단하게 이렇게조립하세요. 소스코딩은 이렇게하면 Bluetooth로 조정이 돼요. 하면 1~2일 post 주제로 끝낼 수 있었지만 아두이노 RC카의 만드는 것보다 만드는 과정을 보여드리고 싶었기 때문에 post가 길어졌네요.

마지막으로 아두이노 RC카의 주행 패턴도 알았고 조정하는 방법까지 알았습니다. RC카의 움직임을 원하는 형태로 여러분들은 할 수 있게 되었습니다. 즉, 어떤 상황이 되었을 때 여러분들이 아두이노 RC카를 특정한 패턴의 움직임을 보이도로 설계가 가능하다는 이야기가 됩니다. 쉽게 말해서, 이 의미는 아두이노 RC카를 상황에 맞게 주행할 수 있도록 자율주행 설계가 가능하다는 의미가 됩니다. 예를들면, 아두이노 RC카 앞에 장애물이 발견되면 그 장애물을 피해서 주행을 자동으로 할 수 있게 할 수 있다는 것이죠.

여러분들이 상상력을 어디까지 끌어 올리느냐에 따라서, 아두이노 RC카 자율주행을 단순하게 또는 정교하게 표현을 가능해 집니다. 한번 아두이노 RC카 자율주행에 대해서 상상의 나래를 펼쳐 보세요.

댓글()

[아두이노] 아두이노 RC카를 조정하는 다양한 코딩 접근법

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

[아두이노] 아두이노 RC카를 조정하는 다양한 코딩 접근법


지난시간까지 해서 간단히 아두이노 2륜 RC카를 조립하고 주행패턴을 분석한 뒤에 Bluetooth를 연결하여 스마트폰으로 조정하는 것 까지 살펴 보았습니다. 오늘은 지난시간에 Bluetooth로 RC카를 조정하는 소스를 가지고 다양한 접근을 시도하고자 합니다. 코딩은 딱 하나의 정해진 로직에 의한 코딩으로 이루어지지 않습니다. 비슷하면서도 다양한 코딩이 존재 합니다. 바로 프로그래머의 주관적 상상에 의해 코딩의 로직이 표현 됩니다. 제가 코딩하는 방식은 제 나름대로의 상상의 의한 코딩입니다. 그렇기 때문에 RC카를 조정하는 코딩은 여러분들이 직접 상상하여 코딩하시면 제가 했던 방식과 다르게 표현 될 수 있습니다. 어떤 회전을 하고 어떤식으로 명령어를 처리해서 DC Motor를 회전 시킬지는 여러분들의 상상력에 달린 것이죠.

오늘은 제가 어떤 상상을 하고 코딩을 했을 때 그 하나의 코딩에 머물지 않고 다양한 방식으로 접근하는 코딩을 설명할까 합니다. 제가 처음 코딩을 입문했을 때 부터 고수해 온 코딩법 입니다. 하나의 결과물을 만들어 내면 거기서 멈추지 않고 유사한 표현이나 다른 접근법을 통해 둘 이상의 결과물이 만들어지도록 코딩 합니다. 그래서인지 대학시절에 조별 프로젝트를 만들면 꼭 2개이상의 결과물을 만들어 제출 했던 기억이 나네요. 저는 할수 있는 코딩만 하고 그 코딩을 통해 표현할 수 있는 최대의 방향으로 코딩을 표현 합니다.

아무튼, 이야기가 삼천포로 좀 빠졌지만 RC카를 조정하는 코딩을 다양하게 접근해 보겠습니다.


1. 2륜 RC카 + Bluetooth 조정 기본 코딩(Switch문)


void loop() {
  if (mySerial.available()){
    char ch = mySerial.read();
        처리문;
  }
}

우선 Bluetooth로 통신을 받는 기본 틀 소스입니다. 처리문은 통신을 통해서 읽은 값을 통해 RC카를 처리하는 위치입니다. 그 위치에서 Switch문을 통해서 명령을 수행하는 소스가 지난시간에 만든 아래 소스입니다.

#include <AFMotor.h>
#include <SoftwareSerial.h>

const int rxPin = A0;
const int txPin = 2;

SoftwareSerial mySerial(rxPin, txPin); // RX, TX

AF_DCMotor motor1(3);
AF_DCMotor motor2(4);
int speed =200;

void setup() {
  mySerial.begin(9600);   
  motor1.setSpeed(speed);
  motor2.setSpeed(speed);
  motor1.run(RELEASE);
  motor2.run(RELEASE);
}

void loop() {
  if (mySerial.available()){
    char ch = mySerial.read();

    switch(ch){
      case 'w': //전진
            motor1.run(FORWARD);
            motor2.run(FORWARD);
            break;
      case 's': //후진
            motor1.run(BACKWARD);
            motor2.run(BACKWARD);
            break;
      case 'a': //제자리 좌회전
            motor1.run(BACKWARD);
            motor2.run(FORWARD);
            break;
      case 'd': //제자리 우회전
            motor1.run(FORWARD);
            motor2.run(BACKWARD);
            break;
      case 'z': //정지
            motor1.run(RELEASE);
            motor2.run(RELEASE);
            break;
      case 'o': //좌회전
            motor1.run(RELEASE);
            motor2.run(FORWARD);
            break;
      case 'p': //우회전
            motor1.run(FORWARD);
            motor2.run(RELEASE);
            break;            
      case 'm': //속도 증가
            speed+=10;
            if(speed>=250) speed=250;
            motor1.setSpeed(speed);
            motor2.setSpeed(speed);
            break;
      case 'n': //속도 감소
            speed-=10;
            if(speed<=0) speed=0;
            motor1.setSpeed(speed);
            motor2.setSpeed(speed);
            break;
    }      
  }
}

지난시간의 코딩한 이 소스에서 간단히 전진/후진, 좌/우회전, 정지 이렇게 5가지 동작만을 코딩으로 간단히 표현 한뒤에 그 소스를 기반으로 연습하셔도 됩니다. 저는 그냥 지난 소스를 기반으로 다른 방식으로 접근하는 코딩을 하겠습니다.

2. IF~Else IF 문


IF~Else IF문은 기본적으로 Switch문과 유사한 동작을 수행합니다.

IF (조건식1) 명령1;
Else IF(조건식2) 명령2;
Else IF(조건식3) 명령3;
...

이런 구조로 되어 있습니다. Bluetooth 통신으로 읽은 값이 조건식1에 만족하면 명령1의 RC카 주행을 하고 만약 조건식1에 만족하지 않으면 다음 라인 조건식2에 만족하는지 체크하고 만족하면 명령2의 RC카 주행을 합니다. 이런식으로 하나씩 비교해서 만족하는 명령의 RC카 주행을 하게 됩니다.

Switch문을 IF~Else IF으로 수정만 하시면 됩니다.

#include <AFMotor.h>
#include <SoftwareSerial.h>

const int rxPin = A0;
const int txPin = 2;

SoftwareSerial mySerial(rxPin, txPin); // RX, TX

AF_DCMotor motor1(3);
AF_DCMotor motor2(4);
int speed =200;

void setup() {
  mySerial.begin(9600);   
  motor1.setSpeed(speed);
  motor2.setSpeed(speed);
  motor1.run(RELEASE);
  motor2.run(RELEASE);
}

void loop() {
  if (mySerial.available()){
    char ch = mySerial.read();

    if (ch == 'w') {
      motor1.run(FORWARD);
      motor2.run(FORWARD);
    } 
    else if (ch == 's') {
      motor1.run(BACKWARD);
      motor2.run(BACKWARD);
    } 
    else if (ch == 'a') {
      motor1.run(BACKWARD);
      motor2.run(FORWARD);
    } 
    else if (ch == 'd') {
      motor1.run(FORWARD);
      motor2.run(BACKWARD);
    }
    else if (ch == 'z') {
      motor1.run(RELEASE);
      motor2.run(RELEASE);
    }
    else if(ch =='o'){
      motor1.run(RELEASE);
      motor2.run(FORWARD);
    }
        else if(ch == 'p'){
      motor1.run(FORWARD);
      motor2.run(RELEASE);
    }
    else if (ch == 'm') {
      speed+=10;
      if(speed>=250) speed=250;
      motor1.setSpeed(speed);
      motor2.setSpeed(speed);
    }
    else if (ch == 'n') {
      speed-=10;
      if(speed<=0) speed=0;
      motor1.setSpeed(speed);
      motor2.setSpeed(speed);
    }
  }
}

이렇게 IF~Eles IF문으로 표현을 합니다.

3. 배열로 동작 명령 제어


다음으로는 배열로 동작 명령을 등록해 놓고 Bluetooth로 수신한 명령 키값을 등록된 배열 명령 키값과 비교하여 일치한 키값에 대한 명령을 수행하는 코딩입니다. 중복된 코딩을 줄이고자 표현한 방식입니다.

이전 시간에 주행 패턴을 만들 때 배열로 했던 기억이 나실지 모르겠네요. 유사합니다. Bluetooth 앱에 조정버턴의 등록된 키 값을 문자열로 등록 합니다.

char stringVal[]="wsadzop"

전진/후진(w/s), 제자리 좌/우회전(a/d), 정지(z), 좌회전/우회전(o/p), 속도 증가/감소(m/n) 입니다. 참고로 속도는 배열에서 제외했습니다.

다음으로는 Motor 패턴 배열입니다. 즉, 두개의 Motor 회전 명령 FORWARD(1), BACKWARD(2), RELEASE(4)의 값을 위의 키값의 순서대로 배열에 위치에 Motor 회전 명령값을 저장합니다.

byte movePattern1[7]={1,2,1,2,4,1,4}; //motor1 Pattern
byte movePattern2[7]={1,2,2,1,4,4,1}; //motor2 Pattern

이렇게 하면, 배열 키값의 index랑 Motor 회전의 index은 같은 위치가 됩니다. 즉, Bluetooth로 수신된 키값이 StringVal[]의 값과 일치한 index(위치)값을 알게 되면 해당 index(위치)값의 Motor 패턴으로 회전 시키면 간단히 키값에 따른 회전 명령을 내릴 수 있게 됩니다.

  if (mySerial.available()){
    char ch = mySerial.read();
        처리문;
  }

에서,

      indexVal = 4;
      for(int i=0;i<7;i++){         
         if(ch==stringVal[i]) indexVal=i;
      }

이렇게 ch값이 순차적으로 StringVal[i]와 비교해서 일치하면 indexVal에 해당 일치한 i값을 저장하게 됩니다. 일치한 키값에 Motor 회전 명령을 수행하기 위해서 index(위치)를 찾는 문장이라고 생각하시면 됩니다. 참고로, for문에 들어가기 전 초기값이 "index=4"인 이유는 4는 정지 명령입니다. 일치한 값이 없을 경우 초기값으로 0을 두면 무조건 전진을 하겠죠. 일치한 동작만 수행하기 위해서는 일치하지 않았을 때 정지해야 겠죠. 그렇게 때문에 정지 위치인 4가 초기값으로 세팅됩니다. 만약 스마트폰에서 다른 알파펫이 들어왔을 때는 일치하지 않기 때문에 정지상태로 머물게 됩니다. 왜! 이렇게 선언했는지 아시겠지요.
만약, index(위치)의 0의 위치에 정지 명령을 넣어두시면 "index=0"으로 초기값을 선언해도 됩니다.

      indexVal = 4;
      for(int i=0;i<7;i++){      
         if(ch==stringVal[i]) indexVal=i;
      }
       motor1.run(movePattern1[indexVal]);
       motor2.run(movePattern2[indexVal]);

이렇게 표현하면, Bluetooth로 수신된 ch값과 일치한 StringVal[i]값의 index를 찾고 그 index의 위치인 movePattern1[index]와 movePattern2[index]의 Motor 회전 명령을 수행하면 키값에 회전 명령을 수행하게 됩니다.

여기까지, 내용은 Bluetooth로 수신된 키값을 회전 비교문이였다면 이제는 속도부분을 처리할 코딩을 작성해야 합니다. 속도는 키값등록 배열에서 제외 했습니다. 그렇기 때문에 Bluetooth에서 수신한 할때 개별적으로 체크해야 합니다. 다음 아래와 같은 형식으로 코딩을 하면 됩니다. Bluetooth 통신에 의해 수신된 ch값이 'm'과 'n' 키와 일치하냐고 묻고 일치하지 않으면 else 이하문으로 해서 방금 위에서 코딩한 방향키 indexVal를 찾는 로직을 수행하면 됩니다.

    if (ch == 'm') {
      speed+=10;
      if(speed>=250) speed=250;
      motor1.setSpeed(speed);
      motor2.setSpeed(speed);
    }
    else if (ch == 'n') {
      speed-=10;
      if(speed<=0) speed=0;
      motor1.run(BACKWARD);
      motor2.run(FORWARD);
    }
    else{
      indexVal = 4;
      for(int i=0;i<7;i++){      
         if(ch==stringVal[i]) indexVal=i;
      }
       motor1.run(movePattern1[indexVal]);
       motor2.run(movePattern2[indexVal]);
     }

속도 내부 로직은 지난시간에 설명을 했기 때문에 생략합니다.

종합해보면,

#include <AFMotor.h>
#include <SoftwareSerial.h>

const int rxPin = A0;
const int txPin = 2;

SoftwareSerial mySerial(rxPin, txPin); // RX, TX

AF_DCMotor motor1(3);
AF_DCMotor motor2(4);

int speed = 200;
int indexVal = 4;
char stringVal[]="wsadzop"
byte movePattern1[7]={1,2,1,2,4,1,4}; //motor1 Pattern
byte movePattern2[7]={1,2,2,1,4,4,1}; //motor2 Pattern

void setup() {
  mySerial.begin(9600);   
  motor1.setSpeed(speed);
  motor2.setSpeed(speed);
  motor1.run(RELEASE);
  motor2.run(RELEASE);
}

void loop() {
  if (mySerial.available()){
    char ch = mySerial.read();
    if (ch == 'm') {
      speed+=10;
      if(speed>=250) speed=250;
      motor1.setSpeed(speed);
      motor2.setSpeed(speed);
    }
    else if (ch == 'n') {
      speed-=10;
      if(speed<=0) speed=0;
      motor1.run(BACKWARD);
      motor2.run(FORWARD);
    }
    else{
      indexVal = 4;
      for(int i=0;i<7;i++){      
         if(ch==stringVal[i]) indexVal=i;
      }
       motor1.run(movePattern1[indexVal]);
       motor2.run(movePattern2[indexVal]);
     }
  }
}

코딩이 엄청 간소화 되었죠. 위에 배열부분은 원하는 형태로 수정하면은 loop()함수를 건들 필요가 없어 수정이 가능합니다.

4. 스마트폰으로 방향키를 누른 상태에서만 동작


스마트폰 앱에서 누르고 있는 동안에만 그 방향으로 회전하고 손을 때면 RC카가 멈추는 것으로 표현하고 싶다면 어떻게 해야 할까요. 키가 누르고 있는 동안에는 그 키 방향으로 RC카가 회전해야 하고 키를 누르지 않으면 정지해 있어야 한다면 어떤식으로 코딩을 하면 좋을까요.

지난시간에 Bluetooth 통신을 통한 조정을 하면서 문득 이런 경우에는 어떻게 제어를 하지 하고 떠오른 상상을 통해서 코딩을 수정해 보았습니다.

예전에 RFID 리더기 코딩을 하면서 카드를 RFID에 대기 전까지 계속 if문으로 무한 체크를 하는 문장이 떠오르더군요.

 if (mySerial.available()){
   처리문;
   return ;
 }
  motor1.run(RELEASE);
  motor2.run(RELEASE);

그냥 이런표현을 하면 되지 않을까 해서 코딩을 해 보았습니다. 즉, 키가 입력되면 처리문을 수행하고 loop()함수문을 빠져나오고 다시 if문으로 Bluetooth의 수신 명령이 있는지 체크하게 하면 되지 않을까 하고 상상을 했습니다. 이렇게 Bluetooth 통신을 통해 수신된 데이터가 없으면 DC 기어모터는 RELEASE로 정지 상태가 되고 수신데이터가 있으면 처리문으로 해당 회전 명령을 수행하게 하면 된다는 상상을 하게 되었네요.

하지만 이렇게 하면 너무 짧은 시간의 찰라에 전진/후진, 좌/우회전을 하고 바로 정지해버리기 때문에 사실 아무런 동작을 수행하지 않게 됩니다. 그래서, 약간의 DC 기어모터가 회전할 딜레이 시간을 주었습니다. 즉, Bluetooth를 통해 전송되는 키값의 딜레이 시간이라 아두이노에서 수신해서 DC 기어모터를 회전 시키는 딜레이시간을 적절이 조절하여 계속 회전 명령을 키를 누를때마다 수행되게 하고 누르지 않으면 정지상태로 되도록 변경했습니다.

참고로, 제가 깐 Bluetooth 앱은 누르고 있으면 연속해서 해당 키값을 전송하지 않기 때문에 일부러 딜레이 시간을 100으로 주고 테스트 했습니다.

#include <AFMotor.h>
#include <SoftwareSerial.h>

const int rxPin = A0;
const int txPin = 2;

SoftwareSerial mySerial(rxPin, txPin); // RX, TX

AF_DCMotor motor1(3);
AF_DCMotor motor2(4);
int speed =200;

void setup() {
  mySerial.begin(9600);   
  motor1.setSpeed(speed);
  motor2.setSpeed(speed);
  motor1.run(RELEASE);
  motor2.run(RELEASE);
}

void loop() {
  if (mySerial.available()){
    char ch = mySerial.read();

    switch(ch){
      case 'w':
            motor1.run(FORWARD);
            motor2.run(FORWARD);
            break;
      case 's':
            motor1.run(BACKWARD);
            motor2.run(BACKWARD);
            break;
      case 'a':
            motor1.run(BACKWARD);
            motor2.run(FORWARD);
            break;
      case 'd':
            motor1.run(FORWARD);
            motor2.run(BACKWARD);
            break;
    }      
    delay(100);
    return ;
  }
  motor1.run(RELEASE);
  motor2.run(RELEASE);
}

이렇게 키를 한번 누를때마다 0.1초동안 회전하게 되는 것이죠. 이것은 Bluetooth 앱이 연속 키를 전달하지 못하기 때문에 회전의 시간을 좀 길게 잡기 위해서 0.1초동안으로 회전을 강제적으로 했지만 연속으로 보낼 수 있는 앱이나 조이스틱이면 delay 시간값을 조정해 주세요. 0.1초도 긴 시간입니다. 1초동안에 300번의 'w'키값이 들어왔다면 1초동안 회전하는게 아니라 3초동안 회전하게 됩니다. 그러면 조정에 문제가 있겠죠. 보내는 시간과 읽는 시간에 대한 회전 딜레이를 조정하시면 원하는 RC카 조정이 이뤄집니다.

[ 결과 ]


키를 누를때 0.1동안 회전시키게 한 RC카 조정하는 영상입니다.


마무리


오늘은 코딩 이야기만 했네요. 하나의 코딩을 했다면 거기서 멈추지 말고 이렇게 상상을 계속 하시면 여러 방법으로 동일한 표현을 할 수 있습니다. 구지 동일한 표현을 다른 방식으로 코딩 할 필요가 있냐고 생각하실 수 있지만 이것은 상상코딩에서 무척 중요한 부분입니다.
코딩하는 사람들은 하나의 상상으로 결과를 얻게 되면 더이상 상상하려 하지 않습니다. 그렇게 되면 하나의 틀에 갇히게 됩니다. 코딩에서는 상상은 무척 중요합니다. 하나의 상상의 틀에 갇히면 발전은 없고 단지 엔지니어가 될 뿐이죠. 계속 이방식으로 코딩해보고 저방식으로 코딩하면서 코딩의 영역을 넓혀가시고 자신만의 색체을 지닌 코딩을 만들어 내셔야 코딩의 성장을 이룹니다.
제가 post하는 소스 코딩은 그냥 따라만 하면 제 코딩 스타일에 여러분들은 갇히게 됩니다. 자신의 코딩 스타일을 찾지 못하게 되는 것이죠. 이방법, 저방법을 시도하면서 자신만의 코딩 스타일을 만들어 내는 것이 무척 중요하니깐 꼭 여러가지 방법으로 접근해 보시면서 자신만의 스타일을 만드셨으면 합니다.


댓글()

[아두이노] 2륜 RC카 Bluetooth를 통해 스마트폰(무선) 조정하기

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

[아두이노] 2륜 RC카 Bluetooth를 통해 스마트폰(무선) 조정하기



지난시간에는 아두이노 2륜 RC카의 주행 패턴을 실험을 해 보았습니다. 오늘은 실험한 주행 패턴을 가지고 Bluetooth로 한번 조정하는 실험을 하겠습니다. 스마트폰에서 Bluetooth 앱을 미리 깔아놓고 그 앱을 통해서 아두이노 2륜 RC카에 부착된Bluetooth로 명령을 내리고 RC카를 조정하게 됩니다. 아래 사진은 아두이노 RC카에 Bluetooth를 연결한 모습입니다.


1. 2륜 RC카 + Bluetooth 회로도


  • 준비물 : Bluetooth, DC 기어모터 2개, L293D Motor Shield, AAx4개 배터리 케이스 배터리 홀더(6V), 아두이노우노

지난시간의 2륜 RC카를 회로도에서 Bluetooth 부품만 추가 연결은 아래와 같습니다.


제가 사용한 L293D Motor Shield에는 따로 Bluetooth를 연결할 수 있는 핀이 없습니다. 대부분 0,1번핀을 이용하여 Serial 통신을 합니다. 전선으로 0, 1번핀을 아두이노와 쉴드 사이에 선을 중간에 묶거나 납탬하는 방법뿐이 없습니다. 대부분 이런식으로 실험을 하지만 자세히 L293D Motor Shield의 기판을 보시면 2번 핀에 구멍이 있고 A0~A5라는 위치에 구명이 뚫려 있습니다. 즉, 핀을 꼽을 수 있는 위치에 핀구멍이 있는데 이 핀구멍을 이용하면 Bluetooth를 쉽게 이용할 수 있습니다.

BlueTooth은 Rx, Tx 핀으로 구성되는데 읽는 핀과 출력 핀으로 나뉩니다. 즉, 2번핀은 디지털핀으로 출력이 가능하고 A0~A5핀은 아날로그핀으로 입력이 가능합니다.

BlueTooth Tx -> Motor Shield A0
BlueTooth Rx -> Motor Shield 2

이렇게 핀 구멍을 활용하면 Bluetooth 통신을 따로 선을 납땜을 할 필요 없이 연결 할 수 있습니다.


2, A0 Pin 위치는 위 사진에서 확인하시고 어느 위치인지 잘 기억해 두세요. Vcc, Gnd pin 위치도 기판에 써있으니깐 보시면 쉽게 찾을 수 있을 꺼에요.

2. 코딩


DC Motor

#include <AFMotor.h>
  • AF_DCMotor motor(3) : M3핀을 DC Motor 제어용으로 사용.
  • motor.setSpeed(200) : 모터 속도 200으로 설정
  • motor.run(FORWARD) : FORWARD, BACKWARD, RELEASE 회전 명령 중 하나를 선택해서 실행 시킴.

SoftwareSerial 통신

#include <SoftwareSerial.h>
  • SoftwareSerial mySerial (rx, tx) : 소프트시리얼 객체선언(rx(수신), tx(전송))
  • mySerial.begin(9600) : 시리얼 통신 시작(예로 9600 통식속도를 사용해 봤네요.)
  • mySerial.write(값) : 데이터 전송
  • mySerial.available() : 데이터 들어왔는 확인
  • mySerial.read() : 전송된 데이터 1byte 읽기

복습으로, Adafruit Industries에서 제공해주는 Motor 라이브러리 함수를 다시 post에 담았습니다. 중요한 부분이니깐 다시 살펴봐 주세요. 그리고 아두이노 내부 시리얼통신 0,1번 핀으로 실험하는게 아니라 다른 핀을 활용하기 때문에 SoftwareSerial 통신 부분도 다시 복습해 주세요.

1) Bluetooth 연결

#include <SoftwareSerial.h>

const int rxPin = A0;
const int txPin = 2;

SoftwareSerial mySerial(rxPin, txPin); // RX, TX

이렇게 해서 SoftwareSerial 통신 객체를 만들었습니다.

void setup() {
  mySerial.begin(9600);   
}

setup()함수안에다 begin()함수로 9600 통신속도로 통신을 시작합니다.

void loop() {
  if (mySerial.available()){
    char ch = mySerial.read();
  }
}

이렇게 loop()함수에서 bluetooth을 통해 들어온 데이터을 읽게 됩니다. available() 함수를 통해 수신 데이터가 있는지 체크합니다. 수신 데이터가 있게 되면 read()함수로 1byte을 읽게 되는데 char 자료형으로 한 문자를 읽어서 저장하게 됩니다.

이미 예전에 다뤘던 내용인데 혹시 잊으신 분들이 있을 수 있기 때문에 다시 설명을 드립니다.

[0,1번 시리얼통신을 할 경우]
따로, SoftwareSerial 라이브러리를 이용하실 필요는 없습니다.

void setup() {
  Serial.begin(9600);   
}
void loop() {
  if (Serial.available()){
    char ch = Serial.read();
  }
}

이렇게 기본 Serial 통신을 이용하시면 됩니다. 0,1번 Pin을 이용할 경우에 이렇게 하고 다른 Pin을 이용할 경우는 SoftwareSerial 라이브러리를 이용하셔야 합니다.

2) 스마트폰에 Bluetooth 앱 설정


안드로이드폰이면 구글스토어에서 Bluetooth 앱을 치시면 아무거나 적당한 것을 다운로드 받아서 설치하시면 됩니다.

앱에서 Controller mode를 선택합니다.



위 그림처럼 버턴에 대한 키값을 지정합니다. 오른쪽 상단에 환경설정 아이콘을 누르시면 아래 창이 뜹니다.


빈칸에 키값을 위에 표시한 알파벳을 등록하시면 됩니다. 그러면 스마트폰에서의 모든 설정은 끝납니다.

3) Bluetooth를 통해 읽은 데이터 값을 통한 동작 제어

Bluetooth를 통해 수신된 데이터 값을 통해서 RC카를 조정하도록 하겠습니다.

전진/후진, 좌/우 명령을 수행한다면 스마트폰 등록된 키값 알파벳 문자들이 수신되면 그 값에 따라서 동작명령을 내려야 합니다.

void loop() {
  if (mySerial.available()){
    char ch = mySerial.read();
  }
}

위 로직에서 if문 안에 수신된 키값에 대한 Motor를 제어하는 명령을 표현 해야 겠죠.

지난 시간 post에서 주행 패턴을 전부 삽입했습니다. 전진/후진과 좌/우만 넣어도 되지만 그냥 실험한 김에 전부 넣었습니다. 여러분들은 전진/후진과 좌/우와 정지 명령만 표현하셔서 실험하세요. 구지 아래처럼 전부 다 넣어서 실험하실 필요는 없습니다.

    switch(ch){
      case 'w':
            motor1.run(FORWARD); //전진
            motor2.run(FORWARD);
            break;
      case 's':
            motor1.run(BACKWARD); //후진
            motor2.run(BACKWARD);
            break;
      case 'a':
            motor1.run(BACKWARD); //제자리 좌회전
            motor2.run(FORWARD);
            break;
      case 'd':
            motor1.run(FORWARD); //제자리 우회전
            motor2.run(BACKWARD);
            break;
      case 'z':
            motor1.run(RELEASE); //정지
            motor2.run(RELEASE);
            break;
      case 'o':
            motor1.run(RELEASE); //좌회전
            motor2.run(FORWARD);
            break;
      case 'p':
            motor1.run(FORWARD); //우회전
            motor2.run(RELEASE);
            break;            
      case 'm':                  //속도증가
            speed+=10;
            if(speed>=250) speed=250;
            motor1.setSpeed(speed);
            motor2.setSpeed(speed);
            break;
      case 'n':                  //속도감소
            speed-=10;
            if(speed<=0) speed=0;
            motor1.setSpeed(speed);
            motor2.setSpeed(speed);
            break;
    }      

위 코딩을 보시면 속도 증가와 감소가 있는데 이 부분은 실제 변화하는 수치는 실험에 사용하는 앱에서는 확인할 수 없어서 불편합니다. 따로 앱인벤터로 전용 Bluetooth를 만들면 되는데 그냥 생략 합니다.

코딩 명령부분만 살펴보면은

speed+=10;
if(speed>=250) speed=250;

예전 post에 표현했던 방법인데 speed를 무조건 10씩 증가시키고 그다음 if문에서 max값으로 해서 그 이상 값이 나오면 max로 무조건 speed 값으로 고정시키는 표현입니다. 그러면 반대로 10씩 감소하면 if문으로 min값 이하 값이 되면 무조건 speed값을 min값으로 고정시켠 되겠죠.

speed-=10;
if(speed<=0) speed=0;

이렇게 어떤 값의 범위을 벗어날려고 하면 min or max 값으로 고정화 시키는 표현은 자주 사용하는 표현이니깐 잘 기억해 두세요. 다른 곳에서 이 표현을 사용할 수 있으니깐요.

4) 종합 소스

지난 시간의 주행 패턴을 Bluetooth 통신을 한다면 다음과 같습니다.

#include <AFMotor.h>
#include <SoftwareSerial.h>

const int rxPin = A0;
const int txPin = 2;

SoftwareSerial mySerial(rxPin, txPin); // RX, TX

AF_DCMotor motor1(3);
AF_DCMotor motor2(4);
int speed =200;

void setup() {
  mySerial.begin(9600);   
  motor1.setSpeed(speed);
  motor2.setSpeed(speed);
  motor1.run(RELEASE);
  motor2.run(RELEASE);
}

void loop() {
  if (mySerial.available()){
    char ch = mySerial.read();

    switch(ch){
      case 'w':
            motor1.run(FORWARD);
            motor2.run(FORWARD);
            break;
      case 's':
            motor1.run(BACKWARD);
            motor2.run(BACKWARD);
            break;
      case 'a':
            motor1.run(BACKWARD);
            motor2.run(FORWARD);
            break;
      case 'd':
            motor1.run(FORWARD);
            motor2.run(BACKWARD);
            break;
      case 'z':
            motor1.run(RELEASE);
            motor2.run(RELEASE);
            break;
      case 'o':
            motor1.run(RELEASE);
            motor2.run(FORWARD);
            break;
      case 'p':
            motor1.run(FORWARD);
            motor2.run(RELEASE);
            break;            
      case 'm':
            speed+=10;
            if(speed>=250) speed=250;
            motor1.setSpeed(speed);
            motor2.setSpeed(speed);
            break;
      case 'n':
            speed-=10;
            if(speed<=0) speed=0;
            motor1.setSpeed(speed);
            motor2.setSpeed(speed);
            break;
    }      
  }
}

지난시간의 패턴을 전부 키에 등록시켜서 실험하기 때문에 switch()문이 꽤! 길게 코딩이 되었네요.

간단히, 동작 FORWARD, BACKWARD, RELEASE 기준으로 Bluetooth 통신으로 읽은 ch(키값)에 해당된 모터 동작 명령을 내리면 됩니다. 주행 패턴이 지난시간에 실험한 것들과 속도까지 컨트롤 하다보니깐 코딩이 길어졌을 뿐 원리는 아래 코딩이 전부입니다.

switch(ch){
 case '키값1':  
            motor1.run(동작1);
            motor2.run(동작1);
            break;
 case '키값2':  
            motor1.run(동작2);
            motor2.run(동작2);
            break;  
}

이 코딩만 이해하시면 됩니다. switch문에서 인수 ch값과 같은 case을 찾고 그 케이스의 라인아래로 명령문들이 수행되는데 break 문을 만나면 switch문을 빠져나오는 로직입니다. 즉, ch값이 키값1이면 motor1.run(동작1)과 motor2.run(동작1) 함수를 수행한뒤에 다음 라인 break문을 만나 switch문을 빠져나오게 됩니다. 만약 동작1을 수행한 뒤에 break문이 없으면 두번째 케이스의 동작2을 수행하게 됩니다. break문이 switch문에서 꼭 필요하니깐 switch문에서 실수하지 말아주세요.

3. 결과


스마트폰으로 조정하면 이렇게 주행을 할 수 있게 됩니다. 이 주행이 정석은 아닙니다. 여러가지 표현 중 하나이고 그 하나를 그냥 소개하는 것일 뿐 다른식으로 조정을 하고 싶다면 한번 코딩에 도전 해보는 것도 괜찮습니다.


마무리


지난 시간의 주행패턴을 전부 사용하다 보니깐 코딩이 길어졌을 뿐 전진/후진, 좌/우 키와 Stop 키로 구성된 5개 키값 만으로 테스트 하셔도 됩니다. 제가 코딩한 것처럼 전부 하실 필요는 없습니다. 좌/우회전 패턴이 2종류인데 원하는 한 종류만 조정값으로 선택하셔도 됩니다.
참고로, speed 코딩이 추가되었는데 어떤식으로 코딩했는지 기억하셨다가 이 원리는 여기뿐 아니라 다른곳에서도 활용이 자주 되는 코딩이라서 원리를 꼭 기억해 주세요.

그렇게까지 어렵지 않습니다. Bluetooth 앱도 구글 검색에서 "앱인벤터 Bluetooth" 키워드로 찾으시면 유튜브 동영상 강좌나 블로그 같은 곳에서 튜토리얼로 잘 나와 있으니깐 따라서 만드시면서 자신만의 Bluetooth 앱으로 개조하셔도 됩니다.

http://ai2.appinventor.mit.edu

이곳에 가셔서 구글계정으로 로그인하면 됩니다. 그리고, 검색하셔서 튜토리얼을 따라서 하시면 됩니다.

이제 여기까지 해서 Bluetooth로 아두이노 RC카를 무선 조정까지 하였습니다. 이제 아두이노 RC카에 어떤 부품을 연결하여 재밌는 것을 만들지 한번 상상의 나래를 펼쳐보세요.


댓글()

[아두이노] 아두이노 2륜 RC카 주행 패턴 실험

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

[아두이노] 아두이노 2륜 RC카 주행 패턴 실험 


지난시간에는 아두이노 2륜 RC카 조립과 시험 주행을 해보았습니다. 이제 본격적으로 주행 패턴을 만들어서 직접 만든 RC카의 주행 동작이 구체적으로 어떤식으로 움직이는지 살펴 볼 차례입니다. 그냥 Bluetooth로 DC Motor 회전의 방향만 조장하는 스마트폰 조정을 바로 할 수 있지만 그전에 자신이 만든 RC카가 어떤식으로 움직이는지 먼저 알아야 합니다. 나중에 직접 조정을 하거나 자율주행을 할 때 꼭 필요한 사전 지식입니다. RC카의 움직임을 미리 알아 두시면 어떤 상황이 될 때 전진 후진, 좌회전, 우회전, 정지 등의 패턴을 쉽게 만들어 낼 수 있게 됩니다. 꼭 필요한 실험이니깐 RC카가 이런식으로 주행 패턴을 만들어 내는구나 하고 배웠으면 합니다.

이제 본격적으로 주행 패턴을 만들어 보도록 하겠습니다.


1. 2륜 RC카 회로도


  • 준비물 : DC 기어모터 2개, L293D Motor Shield, AAx4개 배터리 케이스 배터리 홀더(6V), 아두이노우노

실제 2륜 RC카를 회로도로 살펴보면 아래 그림과 같습니다. 참고용으로 살펴보시기 바랍니다.


2. 아두이노 2륜 RC카 주행 패턴 만들기


DC Motor 2개를 제어하기 하기 위한 함수 motor1.run(), motor2.run() 로 FORWARD, BACKWARD. RELEASE 의 명령을 통해서 주행 패턴이 만들어 낼 수 있습니다.

AF_DCMotor motor1(3);
AF_DCMotor motor2(4);

위 motor 기준으로 왼쪽 모터는 3번 오른쪽 모터는 4번으로 했을 때 다음과 같습니다.

1) 전진과 후진


전진

  motor1.run(FORWARD);
  motor2.run(FORWARD);

후진

  motor1.run(BACKWARD);
  motor2.run(BACKWARD);

2) 좌회전과 우회전


좌회전

  motor1.run(RELEASE);
  motor2.run(FORWARD);

우회전

  motor1.run(FORWARD);
  motor2.run(RELEASE);

3) 제자리 좌회전과 우회전


좌회전

  motor1.run(BACKWARD);
  motor2.run(FORWARD);

우회전

  motor1.run(FORWARD);
  motor2.run(BACKWARD);

4) 정지

  motor1.run(RELEASE);
  motor2.run(RELEASE);

3. 코딩


DC Motor

#include <AFMotor.h>
  • AF_DCMotor motor(3) : M3핀을 DC Motor 제어용으로 사용.
  • motor.setSpeed(200) : 모터 속도 200으로 설정
  • motor.run(FORWARD) : FORWARD, BACKWARD, RELEASE 회전 명령 중 하나를 선택해서 실행 시킴.

Adafruit Industries에서 제공해주는 Motor 라이브러리를 이용하면 setSpeed(), run() 함수로 쉽게 DC Motor를 움직이게 할 수 있습니다.

위에서 주행 패턴에 대해서 간단히 살펴보았습니다. 그 동작을 실제로 코딩해서 실험을 해보도록 할까요.

[ 소스 ]

#include <AFMotor.h>

AF_DCMotor motor1(3);
AF_DCMotor motor2(4);

void setup() {
  motor1.setSpeed(200);
  motor2.setSpeed(200);
  motor1.run(RELEASE);
  motor2.run(RELEASE);
}

void loop() {  
  motor1.run(FORWARD); //전진
  motor2.run(FORWARD);
  delay(2000);
  
  motor1.run(BACKWARD); //후진
  motor2.run(BACKWARD);
  delay(2000);
 
  motor1.run(RELEASE); //정지
  motor2.run(RELEASE);
  delay(2000);

  motor1.run(RELEASE); //좌회전
  motor2.run(FORWARD);
  delay(2000);

  motor1.run(FORWARD); //우회전
  motor2.run(RELEASE);
  delay(2000);

  motor1.run(BACKWARD); //제자리 좌회전
  motor2.run(FORWARD);
  delay(2000);
    
  motor1.run(FORWARD); //제자리 우회전
  motor2.run(BACKWARD);
  delay(2000);
}

주의사항

위 사진을 보면 빨간색으로 표시한 부분이 있는데 덮개처럼 되어 있습니다. 아두이노우노에 프로그램을 업로드 할 때 이 덮개를 빼주세요. 덮개가 그대로 되어 있으면 아두이노우노에서 전원이 공급되는데 Motor Shield에 그대로 전원이 공급되어 프로그램 명령에 따라서 바로 DC Motor가 회전을 하게 됩니다. 덮개를 분리해 놓으면 Motor Shield에 전원이 공급되지 않기 때문에 RC카는 움직이지 않게 됩니다. 매번 실험에 우두이노우노에 프로그램을 업로드 할 때마다 Motor Shield를 아두이노우노에서 분리하지 말고 이 덮개만 빼주면 됩니다.

위에 표시된 덮개는 꼽아져 있으면 아두이노우노와 Motor Shield가 서로 전류를 공유하게 되고 분리하면 전류를 공유하지 않고 개별적으로 전류를 공급 받는 다고 생각하시면 됩니다. 쉽게 말해서 Motor Shield에 전원을 공급하는데 덮개가 되어 있으면 이 전류가 아두이노우노에도 전류가 공급됩니다, 반대로 아두이노우노에 전원이 공급되면 Motor Shield에도 전류가 공급된다고 생각하시면 됩니다. 하지만 덮개가 빠지면 아두이노우노에 전류가 공급되더라도 Motor Shield에 전류를 공급되지 않습니다. 반대로 Motor Shield에 전류가 공급되더라도 아두이노우노에는 전류가 공급되지 않습니다.

위 사진에서 표시한 위치의 덮개를 어떻게 하느냐에 따라서 아두이노우노와 Motor Shield의 전류 공급에 형태를 결정하게 됩니다.

4. 결과




5. 주행 패턴을 배열로 만들기


위 코딩을 보면 순차적으로 길게 나열된 소스입니다. 중복되는 코딩을 보면 뭔가 줄이고 싶은 마음이 생기지 않나요. 저같은 경우는 중복된 코딩을 보면 바로 거것을 배열로 만들어서 코딩량을 줄이고 싶어집니다. 즉, 중복코딩을 보면 바로 배열을 떠오릅니다. 그러면 한번 배열로 전부 만들어 볼까요.

1) 코딩

[패턴] : 2륜 RC카의 motor 패턴만들기

byte movePattern1[7]={1,2,4,1,4,1,2}; //motor1 Pattern
byte movePattern2[7]={1,2,4,4,1,2,1}; //motor2 Pattern
byte delayTime[7]={2,2,2,2,2,2,2}; //motor Rotation time
byte speedPattern1[7]={200,200,200,200,200,200,200}; //motor1 speed Pattern
byte speedPattern2[7]={200,200,200,200,200,200,200}; //motor2 speed Pattern

FORWARD, BACKWARD, RELEASE의 값은 라이브러리 헤더파일에 가면 해당 값을 확인 할 수 있습니다.



위 사이트에 가시면 이렇게 확인이 가능합니다. define으로 정의한 변수명으로 표현할 필요 없이 직접 해당 값으로 표현 해도 됩니다. 보면 BRAKE라고 하나의 변수명이 더 있는데 사실 제가 쓰는 모터쉴드에서는 반응하지 않습니다. 정확히 소스를 살펴봐야 하는데 어떠한 반응도 보이지 않더군요. 그래서, BRAKE만 뺀 나머지 3개의 변수네임만 사용합니다.

실험한 위 소스에서는 Motor1, Motor2의 패턴을 개별 배열 변수로 해서 표현했습니다.

byte movePattern1[7]={1,2,4,1,4,1,2}; //motor1 Pattern
byte movePattern2[7]={1,2,4,4,1,2,1}; //motor2 Pattern

그리고, 회전 시간은 다음과 같이 1초가 1000입니다. 즉, 2초면 2000이 되기 때문에 나중에 delay()함수에서 "2*1000"으로 하면 되기 때문에 간단히 아래와 같이 표현 했습니다. 2초로 고정이니깐 구지 배열변수로 만들 필요가 없지 않냐고 생각 할 수 있지만 나중에 추가로 시간을 제어하기 위해서 우선 만들어 놓았습니다.

byte delayTime[7]={2,2,2,2,2,2,2}; //motor Rotation time

초 단위로 제어를 했지만 만약 좀 더 짧은 시간으로 제어한다면 다음과 같이 표혀을 해야 겠지요.

int delayTime[7]={2000,2000,2000,2000,2000,2000,2000}; //motor Rotation time

이렇게 하고

delay(delayTime[i]*1000);

이 표현에서

delay(delayTime[i]);

이렇게 곱하기 1000 부분을 지워 주면은 되겠죠. 실험은 초 단위로 하기 때문에 byte형으로 간단히 표현 했습니다.

마지막으로 speed 부분입니다. 위 실험에서는 200 speed로 고정이였습니다. 좀 더 다양한 패턴을 실험하기 위해서는 speed에 따른 주행거리를 계산을 해야 합니다. 나중에 speed에 따른 주행거리과 회전각도를 구하기 위해서는 속도와 시간의 값을 통해 수학적으로 계산해야 하기 때문에 측정하기 위해 별로로 배열변수로 다시 선언했습니다.

byte speedPattern1[7]={200,200,200,200,200,200,200}; //motor1 speed Pattern
byte speedPattern2[7]={200,200,200,200,200,200,200}; //motor2 speed Pattern

byte 자료형은 아두이노에서는 8bit 부호없는 숫자 0~255까지 표현이 가능합니다.

[소스]

#include <AFMotor.h>

AF_DCMotor motor1(3);
AF_DCMotor motor2(4);

byte movePattern1[7]={1,2,4,1,4,1,2}; //motor1 Pattern
byte movePattern2[7]={1,2,4,4,1,2,1}; //motor2 Pattern
byte delayTime[7]={2,2,2,2,2,2,2}; //motor Rotation time
byte speedPattern1[7]={200,200,200,200,200,200,200}; //motor1 speed Pattern
byte speedPattern2[7]={200,200,200,200,200,200,200}; //motor2 speed Pattern
int lenghtVal = 7; //pattern lenght

void setup() {
  motor1.setSpeed(0);
  motor2.setSpeed(0);
  motor1.run(RELEASE);
  motor2.run(RELEASE);
}

void loop() {  
    for(int i=0;i<lenghtVal;i++){
        motor1.setSpeed(speedPattern1[i]);
        motor2.setSpeed(speedPattern2[i]);
        motor1.run(movePattern1[i]);
        motor2.run(movePattern2[i]);
        delay(delayTime[i]*1000);
    }
}   

위 결과는 위에서 직접 명령을 내린 코딩과 동일함으로 결과도 동일하게 나옵니다. 위에서 패턴 결과를 동영상으로 찍어서 보여드렸는데 구지 똑같은 영상을 찍을 필요가 없기 때문에 배열 변수로 패턴을 저장하여 실험한 결과물 영상은 생략합니다.

6. 추가 주행


같은 방향으로 회전을 진행하더라도 DC Motor 돌의 speed 값에 따라 진행방향이 바뀌게 됩니다.


대충 이런 느낌이겠죠.

motor1.setSpeed(130);
motor2.setSpeed(200);
motor1.run(FORWARD);
motor2.run(FORWARD);

오른쪽 motor2은 원을 크게 그리게 되고 왼쪽 motor1은 원을 작게 그리며 우회전을 하게 됩니다. 만약 반대면 왼쪽은 큰원을 그리고 오른쪽은 작은원을 그리는 좌회전이 되겠죠.

[소스]

#include <AFMotor.h>

AF_DCMotor motor1(3);
AF_DCMotor motor2(4);

void setup() {
    motor1.setSpeed(0);
    motor2.setSpeed(0);
    motor1.run(RELEASE);
    motor2.run(RELEASE);
}

void loop() {   
    motor1.setSpeed(130);
    motor2.setSpeed(200);
    motor1.run(FORWARD);
    motor2.run(FORWARD);
    delay(1000);    
}   

[결과]


7. 주의 사항


모터 쉴드에 전원을 6V으로 공급하기 때문에 실제 DC 기어모터에 제대로 된 힘을 발휘하지 못합니다. 아두이노에 연결하는 9V 전원 공급쪽으로 하면 DC 기어모터의 회전에 힘을 발휘하기는 합니다. 전원 공급이 약하면 회전력이 떨어집니다. DC 기어모터를 회전시킬 수 있는 힘이 약하기 때문에 speed 값이 낮아지면 정상적으로 해당 speed만큼 회전을 시키지 못합니다.

방금 한쪽은 200을 주고 한쪽을 130정도로 speed 값을 주고 실험한 이유는 100이하로 speed값을 주면 제가 쓰는 DC 기어모터를 정상적으로 회전을 시키지 못합니다. 130의 정도로 해야 겨우 반응을 보이는데 이것도 반응이 없는 경두도 발생했습니다. 싼 DC기어모터의 한계인지 모르겠네요. DC기어모터가 회전을 하려고 하는데 회전 기어를 움직이게 할 힘이 너무 약해서 돌지를 못하는 현상이 발생하더군요. 참고로 아두이노에서 5V usb로 연결하면 speed 일정값이하는 아예 반응하지 않고 DC기어모터에서 이상한 신호음만 크게 납니다. 문제가 있다는 신호인거죠. 싼 DC기어모터여서 그런지 약간 퍽퍽하고 또 전류 공급이 약하면 반응을 잘 못해서 실험에 약간 불편했네요.

그냥 RC카를 조정할 때 6V 전류를 한다면 주행 speed를 200으로 고정해놓고 주행 실험을 하시는게 편할 듯 싶네요. 좋은 모터를 쓰면 그렇게 까지 신경을 안쓰셔도 되겠지만요. 결론은 DC 기어모터의 퍽퍽함과 전류공급에 따라 DC Motor의 회전에 영향을 준다는 것이죠.

마무리


오늘 주행 패턴을 만들면서 사실 6V 전원 공급으로는 DC Motor를 정교하게 제어를 하기 어렵습니다. DC Motor를 오랫동안 사용하지 않아서 처음에는 speed를 200으로 해놓고 실험을 해도 회전을 제대로 시키지 못하더군요. 그냥 건전지를 DC Motor에 다이렉트로 연결하여 회전을 계속 시켰습니다. 그리고 나니 그제서야 DC Motor가 회전하더군요. 기어 부분에게 퍽퍽하고 싼 부품을 구매해서 그런지 제어하기가 좀 불편 했네요.

그래도 어느정도 패턴을 만들어 주행 실험을 하였습니다. 이 주행 결과를 다음 시간에 Bluetooth를 이용하여 스마트폰으로 무선 조정 실험을 할 예정입니다.

참고로, 위에서 주행 배열 변수를 구지 만든 이유는 따로 여러분들이 주행에 대한 실험을 할 수 있는 틀을 마련하기 위해서 입니다. 만약, 주행 정보를 이렇게 나눠 놓고 특정 움직임에 대한 RC카 주행을 기록해 놓는다고 상상해 보세요. 그 기록으로 주행을 시킨다면 어떻게 될까요. 예를들어, 같은 기록 데이터를 가지고 2대 이상의 RC카가 움직인다면 어떻게 될 까요. 아이돌 군무를 상상하면 쉬울 듯 싶네요. 같은 동작을 여러 RC카가 군무를 추듯이 주행을 하게 됩니다. 이런 개념을 갖게 되면은 나중에 군집 주행의 첫발을 내딛을 수 있겠죠.

그리고, 배열에 특정 동작에 대해서 순차적으로 증가 시키거나 감소시키는 값들로 채워 넣는다면 어떻게 될까요. 어떤 패턴 실험의 측정 도구로도 활용이 가능합니다. 시간에 따른 RC카의 주행 거리와 회전 각도들을 시간값의 변화를 배열로 미리 만들어 넣고 실험하여 측정도 가능 합니니다.

실험은 안했지만 여러분들이 한번 다음 실험을 해보셨으면 합니다. 시간과 speed 값에 의한 이동거리와 회전각도를 수학적으로 접근해 보세요. 아니면 그냥 무식하게 speed값을 고정 시키고 시간값에 의한 RC카의 바퀴휠 원둘레 길이를 가지고 시간대 별 이동거리와 회전각도를 눈으로 대충 근사거리와 근사각도를 가늠해 놓고 그걸 기준으로 RC카를 제어를 해보세요. 그러면 좀 더 정교한 제어가 가능해 집니다.

아무튼 오늘은 생각할 부분이 많습니다. 한번 상상의 나래를 펼쳐 보세요.


댓글()