#Arduino #3D printer #아두이노 #3D프린터 # 3D프린터개발산업기사

 

로터리인코더(로타리엔코더) 쉽게 이해하기

 

0. 서론

로터리 인코더는 CLK와 DT라는 두개의 포인트를 가지고 회전을 검출하는 장치입니다.

사실상 원리는 간단하고 이를 아두이노에 활용하기에도 쉽지만 의외로 이 인코더의 작동원리를 이해하기 힘들어 아두이노 코드에 접목하기가 쉽지 않은 것이 현실입니다.

이에 오늘은 로터리인코더(로타리엔코더)의 회전방향 검출에 대해 알아보도록 하겠습니다.

 

1. 로터리 인코더 작동원리

로터리 인코더는 CLK와 DT라는 두개의 포인트로 회전을 검출합니다.

발광부-수광부로 이해해도 좋고, 금속 원판에 포인트가 붙었다 떨어졌다로 이해해도 좋습니다.

어찌되었든 이 두 지점이 거리를 두고 위치하고 있기 때문에 원판이 돌아갈 때 시간차이가 나게 되고, 이 시간차이를 가지고 이 원판이 시계방향으로 회전하는지, 반시계방향으로 회전하는지를 알아낼 수 있습니다.

출처: www.ElectroPeak.com



그림의 이해를 쉽게 하기 위해 원형철판에 두 지점이 닿아있다고 보겠습니다. 이 상황에선 두 포인트 전부 전기가 통하는 상태입니다.(수치로는 1 혹은 HIGH라고 볼 수 있습니다.)
또한 CLK(그림의 Output A)는 DT(Output B)보다 왼쪽에 위치하며, 이미지상으로는 위쪽에 위치합니다.

일단 시계방향으로 돌아갈때를 보죠.
이 상황에서 시계방향으로 돌면 CLK가 먼저 원판에서 떨어집니다. 그리고 그 다음 DT가 떨어지고, 그 다음 다시 CLK가 원판에 붙고, 다시 DT가 붙습니다.
1주기를 살펴보면

CLK DT
1 1
0 1
0 0
1 0
1 1

이렇게 되는 것을 볼 수 있습니다.
또한 최소 분해능은 1/4주기로 볼 수 있습니다. 1/4주기마다 회전을 검출할 수 있기 때문이죠.


그럼 반시계방향으로 돌아가는 상황을 보도록 합시다.
원판이 반시계방향으로 돌면 DT가 먼저 원판에서 떨어집니다. 다음 CLK가 떨어지고, 다음 DT가 다시 붙고, 다음 CLK가 붙겠죠.
1주기를 살펴보면

CLK DT
1 1
1 0
0 0
0 1
1 1

이렇게 되는 것을 볼 수 있습니다.

이를 펄스그림으로 보게되면 아래와 같습니다.

출처: https://protosupplies.com/

 

2. 소자의 이해(HW-040)

저희가 쓸 소자는 HW040이라는 소자입니다. 이 소자의 특징은 로터리 인코더가 한번 딸깍하고 돌아갈때 위에서 말한 1주기 중 1/2주기씩 움직인다는 것입니다. 이를 이미지로 설명하면 원판에 둘다 붙어있다가 한번 딸깍에 둘다 떨어지고, 다음 딸깍에 둘다 붙는다는 말이 되죠. 참고로 첫 시작값은 보통 CLK, DT 모두 1값으로 시작하는게 보통이나 상황에 따라 달라질 수 있기에 소스코드에서 한번 처음 읽어주도록 합시다.

 

3. 코드에의 적용

이렇게 1주기에 CLK와 DT값이 4가지로 변화하기 때문에 이 모든 값에 대응해서 코딩을 해야할 것 같지만(물론 이런 경우가 제일 완벽하겠죠), 자세히 들여다보면 어떤 규칙이 있습니다. 바로 CLK값이 변화할때 시계방향이면 DT와 읽는 값이 정 반대이고(CLK가 왼쪽이므로 원판에서 먼저떨어집니다), 반시계방향이면 CLK값이 변화할때 DT값이랑 같다는 것이죠.(CLK가 왼쪽인데, CLK에서 변화가 감지될 때는 이미 DT값이 변한 이후입니다.)

특히 저희가 쓸 소자는 1번 딸깍에 1/2주기씩 진행하기 때문에 모든 상황에 대해서 코딩하는 것이 낭비라고 볼 수도 있기도 하고, 한번 딸깍에 코딩으로 반응이 와야하기 때문에 모든 주기에 대해서 코딩하기 보단 변화를 가지고 코딩하는게 좋습니다.(1주기 코딩을 하게되면 두번 딸깍해야 반응이 오겠죠)

여기서의 포인트는 딱 두가지입니다.

1) CLK값이 변화할때
2) 그 순간 DT와 CLK값의 차이(정방향 변화: 두 값이 차이, 역방향 변화: 두값이 동일)

 

 그럼 코드를 작성해보겠습니다. 이전과 같이 LED는 각 2, 3, 4번 핀에 대응시키고 로터리 인코더의 CLK, DT는 각 8, 9번 핀에 대응시키겠습니다.(SW는 그냥 스위치처럼 사용하기에 편리하므로 이번 포스팅에서는 제외하겠습니다.) 목표는 로터리 인코더가 오른쪽으로 돌면 빨간불, 왼쪽으로 돌면 파란불이 들어오게 하는 것입니다.

#define LED_R 2 
#define LED_G 3 
#define LED_B 4 
#define CLK 8 
#define DT 9 

bool pre_clk = 0; 

void setup() { 
  pinMode(LED_R, OUTPUT); 
  pinMode(LED_G, OUTPUT); 
  pinMode(LED_B, OUTPUT); 
  pinMode(CLK, INPUT); 
  pinMode(DT, INPUT); 
  pre_clk = digitalRead(CLK); 
} 

void loop() { 
  bool cur_clk = digitalRead(CLK); 
  if(pre_clk != cur_clk){ 
    if(digitalRead(DT) != cur_clk){ 
      digitalWrite(LED_R, HIGH); 
      digitalWrite(LED_G, LOW); 
      digitalWrite(LED_B, LOW); 
    } 
    else{ 
      digitalWrite(LED_R, LOW); 
      digitalWrite(LED_G, LOW); 
      digitalWrite(LED_B, HIGH); 
    } 
  } 
  pre_clk = cur_clk; 
}

#Arduino #3D printer #아두이노 #3D프린터 # 3D프린터개발산업기사

 

millis() 함수와 delay() 함수의 차이

 

0. 서론(매우 빠른 작동속도를 가진 아두이노)

아두이노는 사실 1ms(1/1000초, 밀리세컨드, millisecond)보다 더 빨리 작동하고 있습니다. 물론 내부의 코드 길이나 상황에 따라 또한 어떤 구문을 실행하냐에 따라 걸리는 속도가 달라지기는하겠습니다만 이론적으로 대략 1/16000초의 속도로 한 명령어를 처리(16MHz)합니다.(이쪽으로 더 나가면 플립플롭(Flip-Flop), 래치(Latch), 클록스피드(Clock speed)와 같은 디지털 논리 회로의 영역으로 들어갑니다)

그래서 루프문 안에 시간을 지연시켜주지 않고 켰다 껐다하는 신호만 준다면 아주 단순한 LED를 깜빡이는 프로그램도 LED가 꺼졌다 켜졌다 하는게 눈으로 안보이고 LED가 계속 켜져있는 것처럼 보일겁니다. 약 0.001초마다 스위치를 계속 켰다껐다 하는거니까요.

 

1. 가장 쉽게 쓸 수 있는 delay함수

그래서 가장 쉽게 사용할 수 있는 명령 지연 함수가 delay함수입니다. 이 함수 안에 ms(밀리세컨드)단위의 시간을 입력하면 그 시간만큼 쉽게말해 프로세서는 "정지"하죠.

프로세서가 "정지"하기에 다음 명령 실행까지 시간적으로 지연이 되기는 합니다만, 그 "정지"해 있는 동안엔 프로세서가 아무 일도 못한다는 단점이 있습니다. 말그대로 일시정지하는거죠. 그 시간엔 그 어떤 다른 일도 하지 못합니다.

이런건 단순히 LED를 껐다 키거나 하는 단일 소자로 단일 명령을 시키는데는 전혀 걸림돌이 되지 않지만, 만약 "동시에" 일을 시켜야 하면 난감한 상황이 올겁니다.

 

가령 (delay가 적용된 깜빡이가 있는) 차를 타고가면서 왼쪽 깜빡이를 켰는데,

1) 라이트가 켜진 순간 차가 멈춰버립니다.(불 켜고 딜레이 0.5초=모든 걸 정지 0.5초)
2) 당황하여 깜빡이를 꺼보지만 라이트가 꺼지기 전까진 깜빡이가 꺼지지 않습니다.(깜빡이 스위치를 껐지만 불 꺼지고 딜레이 0.5초 가 지나기 전까지 프로세서는 어떤 입력도 못받아들임)
3) 그리고 깜빡이가 꺼지면서 차가 다시 움직이기 시작합니다.

난감하죠? 100% 교통사고 각입니다.

 

2. delay의 대안 millis함수

그래서 이를 해결하기위해 여러 방법들을 강구하게되고, 그 중에 하나가 millis함수를 이용하는 겁니다.
(여담으로 이를 "병렬처리"라 하고(순차처리=직렬처리, 동시처리=병렬처리), 이를 구현하기 위해서 CPU시스템적인 부분 뿐만 아니라(여기는 진짜로 각 클록 주파수 사이사이에 명령어들을 배치해서 "병렬처리인 척"하는 부분이긴 합니다만..) 각 언어마다 많은 방법들을 만들어냅니다. 싱크로, 메서드 등이 그것들인데 그건 여기서는 제쳐두죠)

아두이노 프로세서는 프로그램이 작동된 후 자동으로 ms단위로 프로그램이 실행되는 시간을 저장하고 있습니다.
따라서 어떤 A라는 순간의 millis값과 B라는 순간의 millis값을 비교하면 시간이 얼마나 흘렀는지 알 수 있게 되죠.(B ms - A ms = 걸린 시간 ms)
[이 부분은 매우 중요한게, millis뿐만 아니라 어떤 프로그램 언어를 사용하든 시간 차이(time delta)는 매우 중요한 개념이 됩니다.]

거기에 millis함수는 delay함수와 다르게 프로세서를 일정기간 멈추는 명령이 아닙니다. 말그대로 시간만 체크하는 명령어죠.(시계 한번 흘깃 보는 명령어입니다.) 이는 엄청난 강점이 됩니다.

 

가령 "10분 있다 엄마가 와서 책상에 앉아있나 확인할거야"라는 명령이 있었다고 보면,

delay: 10분동안 아무것도 안하고 내내 책상에 앉아서 멍때리기
millis: 10분동안 틈틈이 시간보면서, 그 안에서 책도보고 컴퓨터도하다가 10분째 착석

결과는 같지만 10분 활용이 다르죠. millis는 엄마의 명령시간 안에서 이런저런 일들을 "같이(병렬)" 처리했으니까요

 

따라서 millis를 사용하게 되면 경과시간을 알아내어서 시간에 따른 지연 조작을 할 수 있으면서도 내부 명령은 아두이노 속도대로 처리가 되는 병렬처리가 가능해집니다.(프로세서 자체가 멈춰서 시간을 지연하는거나, 사용자가 시계로 체크해서 일정 시간 후를 체크하는 것의 차이?)

 

3. delay함수와 millis함수의 활용

아두이노 자체 적인 개발 외에도 "3D프린터개발산업기사"의 실기부분에도 이 내용이 활용됩니다.

시험 항목을 보죠.

5) HEATING BED 설정
라) 설정온도인 30'C에 도달하면 LED는 R->G->B 순으로 500ms 간격으로 점등 되도록하며, 모터는 시계방향으로 계속적으로 동작 되도록 합니다.

이 부분인데요, 문제를 잘 보시면 LED가 500ms라는 시간을 가지고 점등이 되는 "와중에" 모터는 계속 돌아가야합니다. 병렬로 작동이 되어야 한다는 것이죠. 그럼 여기서는 millis함수를 써야겠죠.

 

반대로 굳이 millis함수를 쓸 필요가 없는 항목도 있습니다.

6) EMERGENCY 설정
나) ~ 근접센서에 물체 또는 손을 이용하여 접촉할 경우 ~ 회전 중인 모터가 정지되고, 부저를 1초 간격으로 5회 울린 후 ~

[회전 중인 모터 정지 -> 부저"" 1초 간격으로 5회 작동 -> ~~]의 순차적인 처리이기때문에 여기서는 굳이 millis안쓰고 delay로 써도 상관이 없습니다.

 

4. 실제 코드 작성

그러면 실제로 응용은 어떻게 해야할까요?

다른 부분은 다 제쳐두고, HEATING BED 설정의 라)항목만 실행하는 프로그램을 작성해보죠(온도가 30도에 도달하여 조건들을 실행하는 부분만 작성했습니다.)

#define LED_R 2  
#define LED_G 3  
#define LED_B 4

unsigned long start_time = 0;

void setup() {  
  pinMode(LED_R, OUTPUT);
  pinMode(LED_G, OUTPUT);
  pinMode(LED_B, OUTPUT);
}

void loop() {  
  start_time = millis(); // 이벤트 시작시간을 저장  
  unsigned long time_delta = millis() - start_time; // 시간차이(time_delta) 설정(현재시간-이벤트시작시간)  
  while(time_delta < 1500*3){ // 시작시간 기준(time_delta가 0부터 증가할 것이므로) (1500ms * 3번)의 시간동안 작동  
    if(time_delta%1500 < 500){ // 시간차이(이벤트 시작부터 진행된 시간)를 1500으로 나눠서 나머지가 500보다 작으면 
      digitalWrite(LED_R, HIGH); // 빨간불 켜기  
      digitalWrite(LED_G, LOW);  
      digitalWrite(LED_B, LOW);  
    }  
    else if(time_delta%1500 < 1000){ // 근데 나머지가 500 이상이고(위의 if가 거짓이기 때문에 else if로 왔으므로) 1000보다 작으면  
      digitalWrite(LED_R, LOW); // 빨간불 끄기  
      digitalWrite(LED_G, HIGH); // 초록불 켜기  
      digitalWrite(LED_B, LOW);  
    }  
    else{ // 그 외의 경우(위에 조건들이 모두 거짓일 때 else로 오므로 나머지가 1000이상인 경우)  
      digitalWrite(LED_R, LOW);  
      digitalWrite(LED_G, LOW); // 초록불 끄기  
      digitalWrite(LED_B, HIGH); // 파란불 켜기  
    }  
    // 모터 작동 코드 작성: delay가 없으므로 매 순간마다 모터가 작동  
    time_delta = millis() - start_time; // 코드 단위를 실행한 후 time_delta 재설정(이벤트 시작부터 진행된 시간 갱신)
  }  
  // 모든 불 끄기  
  digitalWrite(LED_R, LOW);  
  digitalWrite(LED_G, LOW);  
  digitalWrite(LED_B, LOW);  
  delay(2000); // 마무리 확인용 delay(세 번이 제대로 작동됐는지)  
}

이와 같은 코드가 작성될 것입니다.

 

5. 여담

참고로 millis함수는 프로그램 시작 후 50여일 이후의 시간도 ms단위로 기록할정도로(물론 50일경 오버플로나서 다시 0으로 돌아간다지만..) 큰 메모리 용량을 필요로 하기 때문에 unsigned long(4바이트)형태의 자료형을 가집니다.

사실 컴퓨터로 코딩할때는 큰 고려사항이 아닙니다만, 아주 제한된 메모리양과 처리속도를 가진 마이크로프로세서에 코딩할때는 전체적인 메모리와 처리속도를 모두 고려해야하는, 어떻게보면 컴퓨터로 프로그래밍하는 것보다 더 복잡한 과정을 거치게 됩니다.(자료형을 무엇을 쓸지, 프로그램 최적화는 어떻게 해야할지, 변수를 전역으로 두고 쓸지 지역으로 두고 쓸지 등..) 아두이노 기본 프로젝트 같은경우에야 문제가 안되겠지만 프로젝트가 커지면 문제죠. 따라서 프로젝트가 크지 않으면 그냥 unsigned long그대로 써주시되, 어차피 실행시간이 길지 않을 거고 다른 작업들로 메모리가 부족한 상황이다 싶으시면 int(2바이트) 자료형으로 써주시면 됩니다.

##타이머

ATMega 128에서는 4개의 타이머가 존재한다.

각 타이머는 0, 1, 2, 3과 같은 번호를 가지고 있으며, 이 번호는 나중에 레지스터를 할당할 때 사용된다. (ex, TCCR0)

이 중 0, 2번 타이머는 8비트 타이머이고 ATMega 128기준 7372800Hz를 갖는다.

그리고 나머지 1, 3번 타이머는 16비트 타이머이다.


타이머는 보통 세개~네개 정도의 레지스터만 선언해주면 사용이 가능하다. (interrupt 헤더파일은 선언해 주어야 한다.)


TCNTn(Timer/CouNTer register)

 이름에서 보이듯이 직접 타이머를 카운트하는 레지스터이다. 0~255의 값을 가질 수 있으며 만약 이 값을 우리가 임의대로 수정하면, 이후 타이머는 수정된 값 이후부터 더하고, 초기화 되어도 수정된 값으로 초기화가 된다.


OCRn (Timer/Counter Output Compare register n)

 아래에서 설명할 CTC모드와 고속 PWM 모드에서 사용되는 레지스터. 0~255사이의 값을 가지며 타이머의 속도를 조절할 수 있는 역할을 한다.


TCCRn(Timer/Counter Control Register n)

 사실상 타이머를 사용하는데 가장 중요한 레지스터이다.

 구조는 8비트로 | FOCn | WGMn1 | WGMn0 | COMn1 | COMn0 | CSn2 | CSn1 | CSn0 | 이며,

 FOCn(Force Output Compare n)과 COMn0, COMn1(Compare match Output Mode n)은 고급 제어에서 이용되며, 가장 단순하게 타이머 용도로만 사용할 정도면 WGM과 CS만 이해하여도 크게 문제가 없다.

 따라서 이번 포스팅에서는 WGM과 CS에 대해서만 설명한다.

 1) WGMn1 WGMn0 (Waveform Generation Mode n)

WGM의 경우 CS와 다르게 그 위치가 8비트 상에서 연속되어 있지 않으므로, 정확한 위치 확인이 필요하다.

아래 진리표의 경우 WGMn1 WGMn0 순으로 작성된 것으로, 실제 레지스터에 적용시에는 그 구조를 파악하여 정확히 써주어야 한다.

0 0 : 일반 (0~255->0~255) => TCNT레지스터의 값이 0부터 255까지 차례로 증가한 뒤 TOV라는 레지스터로 값이 다 찼다는 결과를 주고 다시 0으로         초기화 된 뒤 255까지 다시 올라가는 방식이다.

0 1 : 위상정정 PWM(Pulse Width Modulation) (0~255~0~255) => TCNT레지스터의 값이 0부터 255까지 차례로 증가한 뒤 일반 모드와는 다르게         바로 0으로 초기화 되는 것이 아니라, 0까지 값을 하나씩 줄여가는 방식이다. 0으로 줄은 상태에서 TOV값이 리턴된다. 물론 0이 되면 다시 값이         1씩 증가한다.

1 0 : CTC(Compare Timer on Compare match) (0~OCRn->0~OCRn) => 다른 방식들과는 다르게 0부터 OCRn 레지스터에 저장된 값까지만 증가하고,         OCRn(Timer/Counter Output Compare register n)레지스터 값과 TCNT레지스터의 값이 동일해지면 TOV를 반환하고 0으로 다시 초기화 된다.

1 1 : 고속 PWM (0~255->0~255) => TCNT레지스터의 값이 0부터 255까지 차례로 증가한 뒤 TOV라는 레지스터로 값이 다 찼다는 결과를 주고 다시         0으로 초기화 된 뒤 255까지 다시 올라가는 방식이다. 일반 모드와 다르게 고속 PWM은 0부터 255까지 증가하지만 OCRn값이 있다면, 그         시기에 즉시 OCn(Output Compare n)값을 리턴한다.

 2) CSn2 CSn1 CSn0 (Clock Select)

프리스케일러(free scaler)라고도 불리며, 분주기라고도 불린다. 이 세 비트의 상태에 따라서 한 클럭마다 신호를 발생할 것인지, 아니면 그것보다 낮은 클럭으로 신호를 발생할 것인지 선택이 가능하다.

0 0 0 : 정지

0 0 1 : 7372800(클럭상태)

0 1 0 : 7372800/8 = 921600(921600클럭으로 돌아가는 것과 같은 상태)

0 1 1 : 7372800/32

1 0 0 : /64

1 0 1 : /128

1 1 0 : /256

1 1 1 : /1024 => 약 71ms로 동작


TIMSK(Timer/Counter Interrupt MaSK register)

 TIMSK = 0x01;로 선언하면 overflow 인터럽트를 Enable한다.


TIFR(Timer/Counter Interrupt Flag Register)

 TOV 발생 시에 기록하는 레지스터.

 TIFR = 0xFF;로 선언하면 첫 인터럽트 시작이 0이 된다.


Sei(); <= 인터럽트 인에이블 함수


SIGNAL(SIG_OVERFLOW0) <= 타임 인터럽트 서비스 루틴


SIGNAL(SIG_OUTPUT_COMPARE0) <= OCR모드일 때 인터럽트 서비스 루틴



* 타이머 조작

1) TCNT를 직접 설정하는 방법 : 한번 설정 시 TCNT부터 255까지 증가

2) 프리스케일러 조작

3) CTC모드로 조작 : OCRn 설정시 0부터 OCRn까지 증가

##인터럽트

AVR에서 인터럽트를 제어하려면 네가지 선언과 한가지 include, 한가지 함수만 사용할 수 있으면 된다.


1. 4가지 선언

 1) 포트개방

 2) EICRA/B

 3) EIMSK

 4) SREG


1) 포트개방

ATMega 128에서는 D0~3번 포트 그리고 E4~7번 포트로만 인터럽트 신호를 받을 수 있다. 따라서 이에 해당하는 포트를 개방시켜주어야 한다.

DDR로 포트개방시 1은 출력 0은 입력을 뜻하므로, DDRD = 0x00; 등으로 선언해 주면 D포트에서 입력을 받겠다는 명령이 된다.

2) EICRA/B (External Interrupt Control Register A/B, 외부 인터럽트 제어 레지스터)

EICR 레지스터는 8비트로 2개 비트씩 한 쌍을 이룬다. 예를 들어, PD0에 대하여 01 00의 두개 비트를 가진다. 그리고 두개의 비트의 조합에 따라

EICRA

00 Low Level

01 예약

10 INT 하강에지

11 INT 상승에지

EICRB

00 Low Level

01 논리변화

10 INT 하강에지

11 INT 상승에지

와 같은 동작을 수행한다.


구조는 다음과 같다.                                      1bit 2bit 3bit 4bit 5bit 6bit 7bit 8bit

EICRA(외부인터럽트 0~3 트리거 설정 [PD0~3]) : | 31 | 30 | 21 | 20 | 11 | 10 | 01 | 00 |

EICRB(외부인터럽트 4~7 트리거 설정 [PE4~7]) : | 71 | 70 | 61 | 60 | 51 | 50 | 41 | 40 |


예를 들어, PD0에 대해서 상승에지에 인터럽트를 감지하고 싶다면

EICRA = 0b00000011; 이라고 선언해주면 된다.

3) EIMSK(External Interrupt MaSK, 외부 인터럽트 마스크 레지스터)

8비트이며 각 포트에서 발생한 인터럽트를 Enable할 것인지(수용할 것인지), Disable할 것인지(무시할 것인지) 판단하는 레지스터이다.

구조는 EIMSK : | INT7 | INT6 | INT5 | INT4 | INT3 | INT2 | INT1 | INT0 | 이다.

예를 들어, PD0에서 발생하는 인터럽트를 수용할 것이라면

EIMSK = 0b00000001;로 선언해주면 된다.

4) SREG(Status REGister, 상태 레지스터)

8비트이며 각 비트는 현재 CPU의 상태를 나타내준다.

최상위 I 비트가 1이 아닌 경우 인터럽트를 받지 않기 때문에, 1로 강제로 세팅해준다.

구조는 SREG : | I | T | H | S | V | N | Z | C |

I 글로벌인터럽트

T storage

H half carry

S sign

V overflow

N negative

Z zero

C carry

즉, SREG |= 0b10000000; 이며, 다른 레지스터와 다르게 원래 SREG와 or 연산을 시키는 이유는 SREG자체가 상태레지스터로 현재 프로그램이외에 다른 상태를 나타낼 수 있기 때문이다.


*) EIFR(Interrupt Flag Register)라는 것도 있는데, EIMSK와 비슷하게 각 자리가 Interrupt 포트와 매치된다. 이 레지스터는 각 포트에서 인터럽트가 발생하였는지 표시하는 레지스터이다.


2. 한가지 인클루드

#include<avr/interrupt.h>

인터럽트를 사용하려면 이 헤더파일을 인클루드 시켜주어야 한다.


3. 한가지 함수

ISR(INT4_vect)

이 함수는 INT4 포트에서 입력되는 인터럽트 신호가 있으면 작동되는 함수이다.

AVR, ATmega128, ATmega 128, LED, ATmel Studio, ATmel Studio 7, 7Segment, 아두이노, 전자전기, FND


ATmega128로 FND 만들고 활용하기!



안녕하십니까?


하루 지나고 다시 글을 올리게 되었습니다.


그 전까지는 사실 배선도그렇고, 개념도 그렇고, 프로그래밍도 그렇고 사실 별로 그렇게 많이 어렵지 않아서 그냥 무작정 따라하기 3단계(배선! 프로그래밍! 실행!)이렇게 스파르타하게 달려왔다면 이번 것은 저도 굉장히 애를 먹었던 부분이라 조금 자세하게 설명하겠습니다.


그 전과는 뭔가 포스팅 분위기부터가 많이 달라졌죠? [몰아서 올리지 않아서 그렇습니다.]


오늘 해 볼것은 FND다루기 입니다.

FND는 Finite Numeric Display의 약자로써 7-Segment(이전 포스팅을 참조 해 주세요!)를 4개 붙여 놓은 것과 같은 형상입니다.


사실 FND와 7-Segment는 서로 교차되서 부르기도 하는데, 1자리 7-Segment를 FND로 부르기도 합니다. (사실 Finite Numeric이니까 길이 제한만 있으면 어떤 Display에 다 써도 될 것 같지만요...)


먼저 준비사항에 대하여 알아보겠습니다.


0] 준비

1) AVR

너무 당연. 항상 ATmega128을 사용하고 있습니다.

2) USB허브

깜빡하고 전 포스팅에서 언급하지 않았었는데, 따로 콘센트-플러그가 있는 USB 허브 제품(그냥 USB하나 달랑 컴퓨터에 꽂는 것이 아니라 거기에 추가로 220V 플러그가 추가로 달려있는)이 혹여 제품에 과도한 전류가 흘렀을 때도 컴퓨터에 영향을 주지 않는다고 합니다.

3) FND

이번에 사용할 모델은 HSN-5643AS-H 입니다. 4자리 7-Segment입니다.

핀은 아래와 같습니다.

여기서 D1, D2, D3, D4는 각 자리를 표시해주는 핀이며, A~G까지의 핀은 각 11, 7, 4, 2, 1, 10, 5에 매치됩니다. 3번 핀은 DP(Dot Point)입니다.

특히 이 모델은 공통 캐소드(CC) 방식으로 각 핀에 주어진 source전류가 한번에 Dx핀으로 빠져나갑니다.

즉, 8개 다 켜지면 sink 전류가 ATmega가 최대로 받아들일 수 있는 전류 100mA 보다 커지므로 필수적으로 저항을 설치해 줘야 합니다. 역기서는 A~DP까지 하나하나 저항을 달아주는 방법과, 각 Drain port에 큰 저항을 4개 써주는 방법이 있겠습니다. 저는 드레인 포트에 큰 저항을 4개 써 주었습니다.

보시면 아시겠지만 A~DP에 해당하는 핀이 1개씩밖에 없으므로 이 4개가 연속된 FND는 순차적으로 불을 켜서 표시하게 됩니다. 즉, 눈의 잔상효과로 인해 사실은 네개가 동시에 켜져있는 것이 아니지만, 인간은 한번에 다 켜져있다고 생각하게 되는 것이죠.

4) 10K 저항 *4

10K 저항을 Drain Port에 달아주었습니다. 너무 어두우면 1K 저항까지도 사용이 가능합니다.


항상 이론과 실제의 오차가 존재하듯이, 실수로 저항 없이 바로 연결이 된 적이 생겼었는데, 다행히도 어떤 부품도 상하지는 않았습니다만... 조심하시는 것이 좋을 것 같습니다.


1] 이 부품의 핀을 보고 배선을 해줍니다.

저번 7Segment와 동일하게 A~DP까지는 PORTA0~7에 연결하였습니다. 그리고 D1~4는 각 PORTG0~3에 연결하였습니다.

각 D1~D4까지는 저항을 달아주었습니다. 브레드보드(빵판)에서는 세로줄이 전부 연결이므로 핀 꽂을때 저항을 달면 옆으로 조금씩밀어주시면 되겠습니다. 같은 라인에 연결하면 전류가 저항 없이 다이렉트로 FND핀에 꽂힙니다. (이렇게 해서 저는 실수했었습니다.)


배선은 그다지 어렵지 않습니다. 그저 시간과 노가다가 오래 걸릴뿐...


2] 프로그래밍!!!!!!

대망의 프로그래밍입니다.

D1~D4는 전류가 빠져나가는 (-)입니다. 다시말해, PORTG0가 1이면 D1에서 전류를 못 흐르게 막는 다는 것이고, 이 말은 D1을 끈다는 소리와 같습니다. 그러면 PORTG0가 0이면 sink 포트가 되면서 전류가 흘러 해당하는 자리에 불이 들어오게 됩니다.

소스는 아래와 같습니다.


#include <avr/io.h>

#include <util/delay.h>

#define F_CPU 16000000UL


int d2b[10] = {0b00111111,

0b00000110,

0b01011011,

0b01001111,

0b01100110,

0b01101101,

0b01111101,

0b00100111,

0b01111111,

0b01101111};


int main(void)

{

    DDRA = 0xFF;

    DDRG = 0b00001111;

    int dp = 0b10000000;

    int flag = 0b00000000;

while(1){

for (int c=3 ; c<13 ; c++){

for (int k=0 ; k<50 ; k++){

for (int i=0 ; i<4 ; i++){

PORTA = d2b[(c-i)%10]|(dp&flag);

flag = ~flag;

PORTG = ~(0b1<<i);

_delay_ms(100);

}

}

flag = ~flag;

}

}

return 0;

}


저번 7segment에서는 각 핀이 (-)역할을 하였으므로 위에서 표시된 0b00111111을 비트 반전시켜서 포트에 출력했을 시 0이 나왔지만, 이번에는 각 핀이 (+) 단자이니 이 코드를 그대로 쓰겠습니다. d2b[0]~d2b[9]는 각 0~9를 나타냅니다.


A포트는 모든 핀을 출력핀으로 사용할 것이니 DDRA = 0xFF;로 모두 개방해줍니다.

G포트는 0~3 핀만 출력핀으로 사용할 것이니 DDRA = 0b00001111;로 첫 4개만 개방해줍니다. 여기서 PORT로 5V를 줬다 뺐다 하는 식으로 조작할 건데 이 부분을 DDR반전 시키고 PIN으로 처리해도 될 것은 같습니다만 제가 해보지 않아서 단정지을 수는 없을 것 같습니다.


dp와 flag는 저번 포스팅에서도 말씀드렸다시피 dp의 깜빡거림을 제어하기 위한 장치였기 때문에 dp와 flag를 모두 빼시고 보시면 더 소스가 간결해집니다. 여유가 되시는 분들은 한번 저 두 변수의 역할에 대해 생각해 보시는 것도 좋은 유흥이 될 것이라 생각합니다.


반복문은 가장 안쪽에서 부터 보는 것이 로직 이해해 도움이 됩니다.

가장 안쪽을 보면 4번 돌아가는 for문이 있는데, 이 것은 대충 눈치 채셨다시피 각 자릿수에 해당하는 것입니다. PORTG를 순서대로 개방하면서 각 PORTG에 해당하는 숫자를 PORTA로 지정해주는 것입니다. 저는 왼쪽으로 밀 것이기 때문에 10개 자릿 수 내에서 돌아가도록 10을 나눠주어 빙글빙글 돌렸습니다.

출력은 D4부터 D1순서로 돌아오므로 D4에 3이 나오게 하기 위해서 c변수를 3에서 출발시켰구요, 자릿수가 오름에 따라서 숫자를 하나씩 줄이기 위해서 c-i를 썼습니다.

_delay_ms()함수를 조절해보면 눈에서 이 문자가 덜 깜빡거리면서 동시에 켜져있는 것처럼 보이게 되는 지점이 있습니다. 대략 125면 어느정도 깜빡이는 수준 안에서 동시에 출력되는 것으로 보입니다. 저는 100이 가장 괜찮았습니다.

그 밖에서 50번 도는 for는 글자를 약 1초간 지속적으로 표시해주는 역할입니다. 왼쪽으로 미는 것처럼 보일 것이기 때문에 숫자가 바뀌는 부분과 표시하는 부분은 따로 진행이 되어야 합니다.

대망의 가장 바깥쪽 for문은 숫자를 증가시켜주는 역할입니다. 가장 오른쪽 숫자를 1씩 증가시키는데, 이렇게 디스플레이하면 왠지 숫자가 옆으로 진행하는 것 같죠.


이렇게 준비가 끝났습니다.

AVR에서 실행시켜보죠!


3] 실행!


자 이렇게 어려운 FND도 마무리 해 보았습니다.


다음에 다시 찾아뵙겠습니다.

#AVR, #ATmega 128, #ATmega128, #LED, #ATmel Studio 7, #아두이노, #전자전기, #ATmel Studio


ATmega128로 연속 LED 시퀀스 만들기!



8개 LED가 연속적으로 변화합니다. 코드를 수정할경우 다른 경우도 만들 수 있습니다.

1) 배선한다!

7-segment보다 덜 복잡합니다. 그냥 PA0부터 1번 LED, PA2-2번LED, ... 순으로 입력해주시면 됩니다. 복잡해 보일 것 같아서 저항은 뺐습니다. (LED가 버틸 수 있는 전압이라 뺐습니다)


2) 프로그래밍한다!

#include <avr/io.h>

#include <util/delay.h>


#define F_CPU 16000000UL


int main(void)

{

int i;

DDRA = 0xFF;

while(1){

for (i=0 ; i<8 ; i++){

PORTA = (0b00000001<<i)-1;

_delay_ms(500);

}

for (i=0 ; i<8 ; i++){

PORTA = (0b11111111<<i);

_delay_ms(500);

}

PORTA = 0x0;

_delay_ms(500);

for (i=0 ; i<8 ; i++){

PORTA = (0b10000000>>i)|PORTA;

_delay_ms(500);

}

for (i=0 ; i<8 ; i++){

PORTA = (0b11111111>>i);

_delay_ms(500);

}

}

return 0;

}

이번 프로그래밍에서 정확하게 1초를 계산할 수 있는 방법이 나왔습니다.

util/delay.h를 인클루드 해 주시고 F_CPU를 16000000UL로 define해주세요. 여기서 16000000은 눈치챈 분도 계시겠지만, ATmega128의 구동 Hz입니다. 기본은 10000000UL로 맞추어져 있으니 다시 define해주시는게 정확하게 1초가 맞게됩니다.

빌드시 warning뜨는데, 재정의 warning이니 무시해주셔도 됩니다.


저는 영상과 같은 모양을 구현하기 위해서 위와같은 코드를 짰습니다.

더 간소화 할 수 있을 것 같은 느낌적인 느낌이 들지만 그냥 적당한 선에서 타협하기로 했습니다.


3) 실행한다!


개인적으로 조금 만족할만한 결과가 나왔습니다.


다음에 다시 뵙겠습니다.

#AVR #ATmega128 #ATmega 128 #LED #ATmel Studio 7 #7 segment #7segment #아두이노 #전자전기


ATmega128로 7-segment LED 조작하기!


애석하게도 따로 찍은 사진이 없어 영상 속 화면으로 대체합니다.



1) 배선한다!

사진 속 7-segment LED는 5611BH 모델입니다. 이 모델은 공통 전원부가 (-)가 아닌 (+)입니다. 즉, 아래 이미지에서 8번 핀에 ATmega 그림에서 아래쪽에서 제일 왼쪽에 있는 VEXT 포트를 연결해주어야 합니다.

저는 A부터 차례대로 PA0와 대응시켰습니다. 즉, A-PA0, B-PA1식으로 DP-PA7로 하였습니다.


2) 프로그래밍 한다!

소스코드입니다.

#include <avr/io.h>


int d2b[10] = {0b10111111,

0b10000110,

0b11011011,

0b11001111,

0b11100110,

0b11101101,

0b11111101,

0b10100111,

0b11111111,

0b11101111};


void delay_secp10(int sec){

volatile int i, j, k;

for(k=0;k<sec;k++){

for (i=0;i<100;i++){

for(j=0;j<1000;j++);

}

}

}


int main(void)

{

    DDRA = 0xFF;

    int dp = 0b10000000;

    int flag = 0b00000000;

while(1){

for (int i=9 ; i>-1 ; i--){

PORTA = ~(d2b[i])|(dp&flag);

delay_secp10(5);

flag = ~flag;

PORTA = ~(d2b[i])|(dp&flag);

delay_secp10(5);

flag = ~flag;

}

}

return 0;

}

d2b배열은 인덱스 넘버가 곧 그 숫자를 표시하는 2진수 바이너리 코드입니다. 즉, 0은 마지막 G핀만 제외하고 나머지가 모두 점등되어야 하므로 0b10111111입니다.

또한 여기서는 delay_secp10이란 함수를 썼는데, 이는 그 전에 delay_sec을 10등분 한 함수입니다. 1초에 dot point를 두번 깜빡여주고 싶어서 만들었습니다.

dp와 flag모두 두번깜빡여주기 위해 만든건데 사실 dp 깜빡임밖에 효과가 없으므로 빼고 진행하셔도 무방합니다.


3) 실행한다!


다음에 뵙겠습니다.

#AVR #ATmega 128 #ATmega128 #LED #ATmel Studio 7 #신호등 #아두이노 #전자전기


ATmega128로 신호등 만들기!


ATmega128이 무엇인지는 현대 인터넷 사회에서 다른 곳에서 엄청 자세히 나와있으므로 일단 실습으로 바로 넘어가겠습니다.


저도 처음입니다!


무작정 따라하기 입니다!


1) 배선한다

2) 프로그래밍 짠다

3) ATmega에 넣어준다!




빨간기판이 ATmega128입니다.

아래쪽 왼쪽에서 두번째가 GND(그라운드)이므로 -선을 연결하여 주시고, 바로 다음부터 있는 PA0번과 1번을 각각 빨간불, 초록불에 대응시켰습니다.


즉, 0번(혹은 1번)이 신호에 따라 +신호를 주고, 이것이 LED로 가서 불을 켜고, GND인 -로 들어오는 회로입니다.


사진에서는 LED가 너무 밝아 각 1K ohm의 저항을 달아주었습니다.


영상에서는 그냥 다이렉트로 꽂혀있는데, 영상쪽이 훨씬 직관적입니다.



아래는 소스코드 입니다.


#include <avr/io.h>


void delay_sec(int sec){

volatile int i, j, k;

for(k=0;k<sec;k++){

for (i=0;i<1000;i++){

for(j=0;j<1000;j++);

}

}

}


int main(void)

{

DDRA = 0b00000011;

while(1){

delay_sec(1);

PORTA = 0b00000000;

PORTA = 0b00000001;

delay_sec(1);

PORTA = 0b00000000;

PORTA = 0b00000010;

}

}


위에서 delay_sec함수는 ATmega128이 초당 16MHz의 처리 속도를 가지고 있는 것을 바탕으로 대략 근사한 값입니다.

정확히 1초는 아니고 1초보다 약간 더 오래 걸립니다.


정확한 1초는 나중에 또 포스팅하겠습니다.


그럼 다음에 또봐요!


+ Recent posts