이전 포스트에서 T/C0에 대해서 소개를 하였다. 여기에서는 관련 IOCR에 대해서 자세히 알아보도록 하겠다.

TCCR0 레지스터

 T/C0가 어떤 클럭 소스을 사용할 것인지는 TCCR0(Timer/Counter Control Register 0)의 b2:0 비트의 CS02, CS01, CS00에 의해서 선택되게 된다.

[그림 1] TCCR0의 비트 구조


TCCR0의 비트 3번부터 7번까지는 예약한 비트들로서 사용되지 않으며 항상 0으로 읽혀진다. 그리고 b2, b1, b0은 다음 표와 같이 T/C0의 프리스케일링 소스를 정의한다.


[표 1] T/C0의 프리스케일 선택

CS02

CS01

CS00

설명

0

0

0

정지, 타이머/카운터0를 정지시킴

0

0

1

clk

0

1

0

clk/8

0

1

1

clk/64

1

0

0

clk/256

1

0

1

clk/1024

1

1

0

외부핀 T0 하강에지

1

1

1

외부핀 T0 상승에지

TCNT0 레지스터

 TCNT0는 클럭 소스의 카운트 값을 실시간으로 저장하는 레지스터로서 읽고 쓰기가 가능하다. 만약 TCNT0에 어떤 값이 쓰여 지면 그 쓰기 동작에 뒤이어 그 값에서부터 계수 작업이 수행되게 된다.


[그림 2] TCNT0 레지스터

이 레지스터는 초기값 $00 (0b00000000)으로 부터 최대값 $FF (0b11111111)까지 순차적으로 업카운팅(up-counting)을 하게 된다. 그리고 최대값 $FF가 되고 나서 그 다음 클럭이 들어오면 $FF + 1 = $00 가 되고 오버플로우(overflow)가 발생하게 된다. 이때 오버플로우 인터럽트가 발생하게 설정이 되었다면 인터럽트가 발생하게 될 것이다.


TIMSK (Timer/counter Interrupt MaSK) 레지스터

 TIMSK는 TC0와 TC1의 인터럽트를 사용할 수 있도록 설정하는 레지스터이다. 여기서는 TC0와 관련된 1번 비트만 확인하자.

[그림 3] TIMSK 레지스터


비트0은 TOIE0 (Timer/counter Overflow Interrupt Enable 0)라고 명명되어 있는데 T/C0에서 오버플로우가 발생했을 때 인터럽트를 발생시킬 것인지 아닌지를 설정하는 비트이다. “0”으로 설정되면 인터럽트를 발생시키지 않으며 “1”로 설정하면 글로벌 인터럽트가 인에이블이면 (즉 SREG의 I비트가 1로 되어 있으면) 인터럽트가 발생된다.

TIFR (Timer/counter Interrupt Flag Register) 레지스터

 TIFR의 구조는 TIMSK의 구조와 유사하며 다음과 같다.

[그림 4] TIFR 레지스터


이 중 TC0와 관련된 비트는 0번이며 만약 TC0에서 오버플로우가 발생하면 b1인 TOV0가 자동적으로 세트가 된다. 이 경우에 TIMSK의 b0가 (TOIE0) “1”로 설정이 되어 있다면 인터럽트가 발생되어 해당하는 ISR 함수가 호출되게 된다.


T/C0와 T/C1의 프리스케일러

 프리스케일링(prescaling)이라는 것은 입력된 클럭의 주기를 정해진 배수로 변형시키는 것이다. 다음 그림 7.2.2에서 보듯이 만약 CK/8이라면 입력 클럭 clk 의 주기가 8배로 늘어나게 됨을 알 수 있다. 만약 CK/64라면 주기가 64배로 늘어날 것이다. 이러한 기능을 수행하는 유닛을 프리스케일러(prescaler)라고 한다. (나누기로 표시되어있어서 주기가 줄어든다고 오해하면 안 된다.)


[그림 5] 프리스케일링의 개념도


T/C0와 T/C1은 같은 프리스케일러 모듈을 사용하지만 설정은 각각 다르게 할 수 있다. 이 프리스케일러는 T/C의 설정과는 무관하게 연속적으로 동작하고 하나의 프리스케일러를 T/C0과 T/C1이 공유한다. 프리스케일러를 리셋하려면 SFIOR 레지스어의 0번 비트 (PSR10)을 ‘1’로 쓰면 된다. 이후에 리셋동작이 끝나면 이 비트는 하드웨어적으로 (자동으로) ‘0’으로 클리어된다.


[그림 6] T/C0와 T/C1의 프리스케일러




Posted by 살레시오
,

 AVR의 내장 장치들 중에서 타이머/카운터는 그 기능이 많고 설정이 복잡하므로 익히는데 노력이 필요한 것 같다. 타이머(timer)는 그 단어가 의미하듯이 시간에 관계되는 것으로 우리가 원하는 시간 간격으로 변하는 신호를 만들어 주거나 특정한 시간에 인터럽트를 발생해 주는 장치이다. 또한 카운터(counter)란 숫자를 세는 것을 의미하며 내/외부의 이벤트(event)나 펄스의 숫자 등을 세는 기능을 가지고 있다. 이러한 기능을 하는 AVR의 내장 장치를 타이머/카운터라고 하며 이후에는 이것을 줄여서 T/C로 표기한다.


 ATmega8(A)에는 세 개의 T/C가 내장되어 있으며 하나는 16비트 T/C이고 다른 두 개는 8비트로 동작한다. T/C의 가장 기본적인 동작은 펄스를 세는 것인데 이 때 이 수를 저장하는 레지스터의 크기에 따라 8비트와 16비트로 구분한다. 즉, 펄스를 255개(8비트의 최대값)까지 셀 수 있다면 8비트 카운터고 65,535(16비트의 최대값)개까지 셀 수 있다면 16비트 카운터이다. 내장된 세 개의 T/C는 0번, 1번, 2번 이렇게 번호가 매겨져 있는데 0번 T/C와 (T/C0) 2번 T/C가 (T/C2) 8비트 T/C이고 이 계수가 저장되는 레지스터가 TCNT0($32번지), TCNT2($24번지)이다. 1번 T/C는 (T/C1) 16비트 T/C이고 그 계수 값은 TCNT1H:TCNT1L 두 개의 레지스터에 걸쳐서 저장된다.


 T/C 기능을 사용하기 위해서는 클럭신호가 필요한데 이것을 입력클럭(CK)로 그대로 사용할 수도 있고 또는 CK를 프리스케일링(prescaling)하여 사용하거나(CK/8, CK/64, CK/256, Ck/1024 등) 외부 핀(TOSC1:0, T1:0)으로 들어오는 클럭 신호를 입력으로 사용할 수도 있다.


T/C0(8비트)

 전술한 바와 같이 ATmega8A는 8비트 T/C 두 개를 내장하고 있으며 각각 T/C0, T/C2으로 표기한다. 이번에는 그 중 기능이 제일 단순한 T/C0에 대해서 알아본다.


 T/C0는 시스템 클럭(CK), 프리스케일된 클럭(CK/n) 또는 외부핀(T0:6번 핀)으로부터 입력되는 클럭 신호들 중 하나를 선택하여 사용하게 된다. 선택된 클럭을 clkT0 라고 표기한다. [그림 1]의 제어 유닛(control unit)은 클럭 입력을 계수하여 TCNT0레지스터의 값을 하나씩 증가시킨다. 그리고 오버플로우 인터럽트가 설정되어 있다면 TCNT0가 $FF에서 $00으로 변하는 순간 인터럽트(TOV0)를 발생시키기도 한다.

[그림 1] T/C0의 기능별 블럭 다이어그램

T/C0에 관련된 I/O레지스터로는 다음과 같은 것들이 있으며, 다른 IOCR과 마찬가지로 그 기능을 나타내는 말의 줄임말로 이름을 붙였다.


  • TCNT0 : Timer/CouNTer 0

  • TCCR0 : Timer/Counter Control Register 0

  • TIMSK : Timer/counter Interrupt MaSK 0

  • TIFR : Timer/counter Interrupt Flag Register


[그림 2] T/C0와 관련된 핀들


위 그림은 T/C0와 관련된 핀(T0핀)을 표시한 것이다.



Posted by 살레시오
,

 이번에는 스위치와 7세그먼트를 이용하여 1에서 6사이의 숫자를 임의로 표시해 주는 전자주사위를 구현해 보겠다. 처음에는 7세그먼트의 테두리가 빙글빙글 돌고 있다. SW1을 누르면 임의로 1-6사이의 숫자가 빠르게 표시되다가 점점 느려진다. 이 때 숫자가 표시될 때마다 부저가 짧게 울린다. 마지막 숫자가 표시되면 2초 동안 멈추어 있다. 이후 다시 테두리가 돌아간다.


#define F_CPU 16000000
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include <stdlib.h>
#include "Am8USBasp.h"
void _delay_ms_var(uchar ucA);
#define NUMREPEAT 20
volatile uchar ucFlag = 0;
ISR(INT1_vect) {
   ucFlag = 1;
}
int main(void) {
   uchar ucaDice[6] = {0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d};
   uchar ucaCirc[6] = {0x03, 0x06, 0x0C, 0x18, 0x30, 0x21};
   uchar ucA=0, ucPrev, ucB, ucC;
   uint uiSeed = 0;
   InitAM8();
   GICR|=0b10000000; // External Interrupt(s) initialization
   MCUCR=0b00001000; // INT1: Falling Edge
   GIFR =0b10000000; // set interrupt flag as '0'
   sei();
   while(1) {
       if (ucFlag == 0) {
           ucA += (ucA==5) -5:1;
           SEG(ucaCirc[ucA]);
           _delay_ms(100);
           uiSeed++;
       } else { // if (ucFlag = 1)
           srand(uiSeed);
           ucPrev = rand()%6;
           for(ucB=1;ucB<=NUMREPEAT;ucB++) {
               do {
                   ucC = rand()%6;
               } while (ucC==ucPrev);
               SEG( ucaDice[ucC] );
               Beep;
               if (ucB == NUMREPEAT)
                   _delay_ms(2000);
               else
                   _delay_ms_var(ucB);
               ucPrev = ucC;
           }
           ucFlag = 0;
       } // else
   } // while(1)
}
void _delay_ms_var(uchar ucA) {
   do {
       _delay_ms(10);
   } while (--ucA>0);
}


이 예제에서 보면 while(1) 반복문이 크게 두 부분으로 나뉘어 있다는 것을 알 수 있는데 ucFlag==0 일 경우와 ucFlag==1 일 경우이다. 전자의 경우는 7세그먼트의 테두리가 돌아가도록 되어 있고 후자의 경우는 숫자들이 반복되어 표시되면서 주사위 숫자(1~6)중 하나를 표시하도록 되어 있다. 전역변수 ucFlag는 SW1이 눌려지면 ISR()함수 내부에서 1값으로 바뀌게 되어있으므로 volatile 키워드를 붙여서 정의하였음에 유의해야한다.


volatile uchar ucFlag = 0;


그 다음으로 설명할 점은 rand()함수의 사용법인데, 이 함수는 stdlib.h 함수에 정의되어 있으며 호출될 때마다 0 에서 RANDOM_MAX 상수 ( 0x7FFFFFFF 값으로 stdlib.h에 정의되어 있음) 사이의 정수를 임의로 발생시키는 함수이다. 그런데 이 함수는 srand()함수로 초기화를 시킨 이후에 사용을 해야지만 진정한 난수를 발생시킬 수 있다. srand()함수를 임의의 시드값으로 초기화 시키지 않으면 시스템이 재시작될 때마다 똑같은 난수가 발생하므로 진정한 난수라고 할 수 없는 것이다. 보통 PC환경에서는 이 시드값으로 시스템의 시간관련 값을 사용하는데 여기서는 그럴 수 없으므로 uiSeed변수를 두어 이 변수값은 계속 증가하게끔 하였다. 그리고 srand()함수를 초기화시킬 때 uiSeed 변수를 두어서 버튼이 눌려질 때 이 값으로 초기화를 시켜주면 그 시점에서의 uiSeed변수 값은 스위치를 누른 시점에 의존하므로 누를 때마다 달라질 것이다. 이렇게 하여 버튼이 눌려질 때마다 다른 시드값으로 초기화되어 진정한 난수가 발생되게 된다.


 그리고 _delay_ms()함수는 입력으로 상수 값만을 받으며 변수 값을 입력으로 줄 수 없다. 따라서 숫자가 표시되는 시간을 점점 늘려가도록 하는데 사용하기 위해서 _delay_ms_var()함수를 별도로 정의하여 (들어온 변수값 × 10ms) 시간 동안 지연할 수 있도록 작성하였다.


 이 프로그램을 PC에서 컴파일하면 958byte (전체 플래시롬의 약 11%)의 실행파일이 생성되었다. 따라서 ATmega8(A)에 꽉 차는 정도의 길이로 프로그램을 하려면 이 예제의 약 10배 정도 (대충 A4용지 10페이지 분량)는 작성해야 함을 알 수 있다.



Posted by 살레시오
,

 외부 인터럽트를 실험하기 위해서 INT1핀에 택스위치를 연결한 회로도를 [그림 1]에 도시하였다. 풀업 저항이 AVR내부에 내장되어 있기 때문에 외부에 별도로 풀업 저항을 달아주지 않아서 회로가 굉장히 간단해짐을 알 수 있다. 대신 내부에서 이 두 핀에 풀업 저항을 연결하려면 DDRD3 = '0', PORTD3 = '1'로 설정하여야 한다.


[그림 1] 외부 스위치 회로도


전기적인 접점을 갖는 스위치를 연결하여 사용할 때 꼭 고려해야 될 사항이 있는데 그것은 진동(bouncing) 현상이다. 이것은 전기적인 접점이 닫히거나 열릴 때 그 양단의 전기적인 신호가 깔끔하게 ‘1’에서 ‘0’으로 또는 ‘0’에서 ‘1’로 변하는 것이 아니라 아주 짧은 동안에 접점이 열렸다 닫히기를 반복하는 현상이다. [그림 2]는 접점이 떨어지는 순간의 진동 현상을 보여준다.


[그림 2] 기계적인 접점을 갖는 스위치의 바운싱 현상


이러한 바운싱을 적절히 처리하지 못하면 매우 빠른 속도로 처리되는 uC에서는 스위치가 여러 번 눌려진 것으로 인식하며 이로 인해서 ‘rising edge'나 ’falling edge'에서 인터럽트가 의도하지 않게 여러 번 발생하게 된다. 이러한 기계적인 접점의 진동을 제거하는 것을 디바운싱(debouncing)이라고 하며 크게 하드웨어적인 회로를 이용하는 법과 소프트웨어적인 방법 두 가지로 나뉜다. 많이 사용되는 하드웨어 디바운싱회로를 [그림 3]에 도시하였다.

[그림 3] 디바운싱 회로의 예들 (a) 좌측, (b) 우측


[그림 3]의 (a)회로도를 보면 스위치 양단에 커패시터를 병렬로 달아서 전압 리플을 억제해주는 가장 간단한 회로이다. (b)회로도는 여기에 슈미트 트리거를 추가하여 작은 리플도 제거해주는 회로이다.


 소프트웨어 디바운싱은 하드웨어의 추가 없이 프로그램으로 진동을 제거하는 방식으로 진동이 보통 10ms(길어야 20ms)정도 지속된다는 점에서 착안하여 처음 감지된 이후 일정 시간이 지난 후에도 그 값이 유지가 되는지를 확인하는 알고리즘을 프로그램으로 작성하는 것이다.


 여기에서는 바운싱을 제거하기 위해서 푸시 스위치와 병렬로 1uF의 커패시터를 각각 연결한 가장 간단한 회로를 택하였으며 이 커페시터가 양단의 전압이 급격하게 변하는 것을 어느 정도 억제해 준다.


 내부에 풀업 저항을 연결하였다면 스위치가 안 눌려졌을 때는 INT0/INT1핀으로 ‘1’신호가 입력이 될 것이다. 만약 스위치가 눌려졌다면 이 핀들은 GND에 연결이 되기 때문에 ‘0’신호가 읽려진다. 따라서 스위치를 누르는 순간 1→0으로 변하게 되고 이렇게 변하는 순간을 하강 엣지라고 한다. 반대로 스위치를 누르고 있다가 떼는 순간 0→1로 신호가 바뀌게 되며 이러한 순간을 상승 엣지라고 한다.


스위치 실험 1

 첫 번째 스위치 실험으로 SW1을 누를 때마다 짧게 부저가 울리면서 LED의 상위 니블과 하위니블의 위치가 바뀌는 프로그램을 작성해 보자. 인터럽트는 하강 엣지, 즉 스위치를 누르는 순간에 걸리도록 설정했다.


#define F_CPU 16000000
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include "Am8USBasp.h"
volatile uchar ucLed = 0x0F;
ISR(INT1_vect) {
   BuzzOn;
   _delay_ms(10);
   BuzzOff;
   LED( ucLed=~ucLed );
}
ISR(BADISR_vect) {}
int main(void) {
   InitAM8();
   GICR|=0xC0; // External Interrupt(s) initialization
   MCUCR=0x0A; // INT0: Falling Edge, INT1: Falling Edge
   GIFR=0xC0;
   sei();
   LED(ucLed);
   while(1);
}

 이 예제를 보면 먼저 main()함수 내부의 while(1);에서 무한루프에 빠진다는 것을 알 수 있다. 즉, main()함수 내부의 동작은 여기에서 멈추지만 SW0을 누를 때마다 하드웨어적으로 인터럽트가 걸리게 되고 ISR(INT0_vect)함수가 호출이 된다. 이 함수 내부에서 부저를 짧게 울리고 LED출력(PORTB)를 반전시킨 후 다시 main()함수로 돌아와 무한루프를 계속 돌게 된다. 이것이 인터럽트가 운용되는 기본적인 구조이다. 여기에서는 단순히 무한루프에 빠져 있지만 중요한 점은 main()함수 내에서 스위치의 동작을 검출하려는 어떠한 코드도 없다는 것이다.

스위치 실험 2

 두 번째 스위치 실험으로 SW1을 누르면 LED가 위로 한 칸씩 움직이고 끝까지 가면 다시 처음 위치로 되돌아가는 프로그램을 작성해 보자. 이전 실험과 마찬가지로 스위치를 누를 때마다 부저가 짧게 울리게끔 해보자.


#define F_CPU 16000000
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h>
#include "Am8USBasp.h"
volatile uchar ucLed = 0x01;
ISR(INT1_vect) {
   ucLed = (ucLed == 0x80) 0x01:(ucLed<<1);
   LED(ucLed);
   Beep;
}
ISR(BADISR_vect) {}
int main(void) {
   InitAM8();
   GICR|=0xC0; // External Interrupt(s) initialization
   MCUCR=0x0A; // INT1: Falling Edge
   GIFR=0xC0;
   sei();
   LED(ucLed);
   while(1);
}



Posted by 살레시오
,

 ATmega8A의 외부 인터럽트를 받아들이는 핀은 [그림 1]에 표시된 바와 같이 두 개가 있다. ‘외부 인터럽트를 받아들인다’는 것은 쉽게 설명하면 이 핀에 걸리는 전압의 변화를 감지하여 그 시점에서 특정한 일을 수행토록 할 수 있다는 의미이다. 예를 들어 INT0핀(4번핀)의 전압이 5V에서 0V로 떨어지는 순간 ISR함수를 호출한다든가 혹은 0V에서 5V로 올라가는 순간 ISR 함수를 실행토록 할 수 있다는 말이다. 이러한 외부 인터럽트 핀을 이용하여 버튼이 눌려지는 것을 감지한다든가 혹은 센서의 측정값이 변하는 것을 감지한다든가 하는 응용을 할 수 있다.


[그림 1] ATmega8(A)의 외부 인터럽트핀


일반적으로 PD2:3핀을 외부인터럽트로 사용하려면 외부 소자에서 신호를 읽어들여야 하기 때문에 방향을 입력(DDRD2='0', DDRD3='0')으로 설정한다. 하지만 방향을 출력으로 설정해 놓아도 인터럽트를 발생시킬 수 있는데 이 경우는 PORTD2 비트나 PORTD3 비트를 조작하여 (즉, 외부 요인이 아니라 내부 프로그램에 의해서) 그 신호 변화에 따라서 내부적으로 인터럽트를 발생시킬 수 있다. 이러한 방식을 ‘소프트웨어 인터럽트’라고 한다.


 관련된 레지스터들로서 일단 MCUCR(MCU Control Register)이 있다.

[그림 2] MCUCR

[그림 2]에서 외부 인터럽트와 관련된 비트는 0번부터 3번까지 4비트이다. ISC01:0 두 비트로 INT0의 동작을 설정하며 ISC11:0 두 비트로 INT1의 동작을 설정한다.


[표 1] 인터럽트 0/1의 발생 제어

ISCn1

ISCn0

동작

0

0

INTn의 low level에서 인터럽트 발생

0

1

INTn의 신호가 변하면 인터럽트 발생

1

0

INTn의 ‘falling edge’에서 인터럽트 발생

1

1

INTn의 ‘rising edge’에서 인터럽트 발생


ISCn1:0='00'이면 INTn핀이 '0'값이면 인터럽트가 무조건 발생한다. 즉, INTn핀이 ‘0’이 되면 ISR(INTn_vect)함수를 호출하고 이 함수가 끝나고 나서도 또 ‘0’이라면 다시 인터럽트가 발생해서 ISR(INTn_vect)함수를 다시 호출하게 된다. ISCn1:0='10'이면 하강 엣지(edge) 에서 인터럽트가 발생하게 되는데 ’1‘에서 ’0‘으로 변하는 순간을 하강 엣지라고 한다. 반대로 ISCn1:0='11'이면 상승 엣지에서 인터럽트가 발생하게 되는데 ’0‘에서 ’1‘로 변하는 순간을 상승 엣지라고 한다. ISCn1:0='01'로 설정하면 하강 엣지와 상승 엣지 두 경우 모두에서 인터럽트가 발생하게 된다. 다음 <그림 6.3.2>에 이들 각각의 경우를 도시하였다. 이 그림에서 하단의 위쪽으로 향하는 화살표는 인터럽트 요구를 표시한다.


[그림 3] 외부 인터럽트 동작 모드


GICR 레지스터의 구조는 다음과 같다.


[그림 4] GICR 레지스터


INT0비트가 세트(‘1’)되고 SREG의 I비트가 세트(‘1’)되었다면 외부 인터럽트 0번은 활성화된다. 마찬가지로 INT1비트가 세트(‘1’)되고 SREG의 I비트가 세트(‘1’)되었다면 외부 인터럽트 1번은 활성화된다.


 외부 인터럽트를 활성화시키기 위한 SFR의 설정을 다음과 같이 요약할 수 있다.

       ( 필요하다면 PD2(PD3)핀에 내부 풀업 저항을 연결한다.)

       ① 인터럽트 0번(1번)의 동작을 MCUCR을 설정하여 정한다.

       ② GICR레지스터의 INT0(INT1)비트를 세트시킨다.

       ③ SREG레지스터의 I비트를 세트시킨다.

이렇게 설정이 완료된 시점 이후부터는 INT0(INT1)핀에서 전기적인 신호가 변하면 인터럽트가 발생하게 된다.



Posted by 살레시오
,

 AVR툴체인으로 인터럽트를 처리하기 위해서 정해진 형식을 가지는 함수를 작성해야 한다. 어떤 인터럽트가 발생했을 때 이를 받아서 처리하는 함수를 ISR함수라고 하며 이 형식은 <interrupt.h>헤더파일에 정의되어 있다. 따라서 인터럽트를 사용하기 위해서는 이 헤더파일을 반드시 인클루드 시켜줘야 하며 ISR 정의 형식은 다음과 같다.


#include <avr/interrupt.h>
ISR(<vector>) {
   // 함수의 본체
}


여기서 <vector>는 발생한 인터럽트가 어느 것이냐에 따라 미리 정해진 식별자이며 ATmega8A의 경우 다음 표와 같이 정의된다.


[표 1[ ATmega8(A)의 인터럽트 벡터

번호

<vector>

인터럽트 종류

비고

1

INT0_vect

외부 인터럽트 0번

외부

발생

2

INT1_vect

외부 인터럽트 1번

3

TIMER2_COMP_vect

타이머/카운터2 비교매치

내부

발생

4

TIMER2_OVF_vect

타이머/카운터2 오버플로

5

TIMER1_CAPT_vect

타이머/카운터1 캡춰

6

TIMER1_COMPA_vect

타이머/카운터1 비교매치A

7

TIMER1_COMPB_vect

타이머/카운터1 비교매치B

8

TIMER1_OVF_vect

타이머/카운터1 오버플로

9

TIMER0_OVF_vect

타이머/카운터0 오버플로

10

SPI_STC_vect

직렬 전송 완료

11

USART_RXC_vect

USART 읽기(Rx) 완료

12

USART_UDRE_vect

USART 데이터레지스터 비워짐

13

USAER_TXC_vect

USART 쓰기(Tx) 완료

14

ADC_vect

ADC 완료

15

EE_RDY_vect

EEPROM 준비완료

16

ANA_COMP_vect

아날로그 비교

17

TWI_vect

TWI 인터페이스

18

SPM_RDY_vect

프로그램메모리 쓰기 준비완료


AVR-Toolchain에서는 ISR을 작성하지 않은 인터럽트가 발생하면(즉, 인터럽트 발생은 허용시켰는데 해당하는 ISR함수가 없다면) 리셋 벡터로 점프하도록 내부에서 처리하고

있다. 이것을 회피하려면 다음과 같이 ISR함수를 정의하면 ISR함수가 없는 모든 인터럽트를 처리하도록 할 수 있다.


#include <avr/interrupt.h>
ISR(BADISR_vect) {
   // 함수의 본체
}


또한 인터럽트와 관련된 매크로함수로서 sei(), cli() 함수가 있다. sei()함수는 인터럽트 발생을 전역적으로 허용하는 것이고, cli()함수는 반대로 인터럽트의 발생을 전역적으로 허용치 않도록 설정하는 함수이다.


 어떤 경우에는 ISR()함수와 다른 함수 (예를 들어서 main()함수) 들과 변수를 공유할 수도 있다. 이 경우에는 그 변수를 반드시 전역변수로 선언해야 하며, 한 가지 주의할 점은 만약 ISR()내부에서 이 변수가 갱신된다면 반드시 volatile 키워드를 붙여서 선언해야 한다는 점이다. 다음의 예를 보자.


#include <avr/interrupt.h>
int myValue; //<-
ISR(INT0_vect) {
   myValue++;
}
int main(void) {
   ⋮
   while (myValue == 0); // wait for interrupt
       TurnLEDOn;
   ⋮
}


위의 예에서 main()함수의 while()문 안에서는 myValue 변수 값이 변하지 않으므로 무한루프에 빠지게 된다. 이것의 내부적인 동작 방식을 보면 myValue==0 조건을 검사하기 위해서 맨 처음에는 myValue값을 읽어서 레지스터에 저장하고 그 다음 반복부터는 그 레지스터에 저장된 값(맨 처음 읽어들인 값)을 0과 비교하게 되므로 값이 ISR( )함수에서 갱신된 값이 반영이 안 되는 것이다. 따라서 무한루프에 빠지게 되고 여기서 프로그램이 멈추게 된다. 하지만 전역변수 myValue를 다음과 같이 volatile형으로 정의하면 매 반복마다 myValue값을 다시 읽어 들이게 되므로 ISR( )함수 내부에서 갱신된 값이 반영이 된다.


volatile int myValue;


 참고로 AVR은 하드웨어적으로 인터럽트 처리가 시작되면 SREG의 글로벌 인터럽트 프랙 I가 자동적으로 0이 되어 인터럽트처리가 끝날 때까지 추가적인 인터럽트는 발생하지 못하는 상태가 된다.



Posted by 살레시오
,

 마이크로컨트롤러(uC)가 어떤 작업을 수행하고 있는 도중에 특수한 이벤트가 발생하면 수행 중인 작업을 중단하고 발생한 이벤트를 처리한 후 이전에 수행하던 작업으로 되돌아가 나머지 작업을 계속 처리하게 된다. 이러한 ‘즉시 처리해야 하는 특수한 이벤트의 발생’을 인터럽트(interrupt)라고 한다. 인터럽트가 발생하면 uC는 그것을 처리하는 작업들이 수록된 함수로 명령 수행 과정을 옮기게 되는데 이 함수를 인터럽트 서비스 루틴 (interrupt service routine, 이하 ISR)이라고 한다

.

 인터럽트는 uC시스템에서 굉장히 중요한 비중을 차지한다. 비유를 들어서 설명하자면 어떤 전화기가 있는데 전화가 걸려왔는데도 벨소리도 안 나고 진동도 안 된다고 가정해보자. 이 전화기로 꼭 받아야만 하는 중요한 전화가 올 예정이라면 그 사람은 다른 일은 못하고 몇 초에 한 번씩 전화가 왔나 안 왔나 들어서 확인해 봐야 할 것이다. 하지만 전화가 걸려왔을 때 벨소리가 나거나 진동하는 전화라면 편하게 다른 일을 하고 있다가 전화가 오면 벨소리를 듣고(인터럽트 요구) 전화를 받아 용무를 처리한 후 하던 일을 계속할 것이다.


 이전 포스트에서 사용된 버튼을 가지고 더 설명하면 ‘버튼이 눌려졌다.’라는 특수한 이벤트를 감지하려면 uC가 main()함수 내에서 항상 주기적으로 이 버튼의 상태를 검사했어야 했다. 하지만 이것을 인터럽트로 처리하면 uC입장에서는 이 버튼의 상태를 항상 검사할 필요가 없이 다른 일을 할 수가 있는 것이다. 다른 작업을 하고 있다가 ‘버튼이 눌려지면’ 그 즉시 인터럽트를 발생하여 하던 일을 멈추고 이를 처리할 ISR을 호출한 뒤 ISR이 끝나면 하던 일을 다시 하면 되는 것이다.


 이와 같이 (외장 혹은 내장) 주변기기와 uC가 통신을 수행하는데 있어서 uC가 주변기기의 상태를 메인루틴에서 상시 검사하지 않고 인터럽트로 처리하면 전체적인 시스템의 운용 효율이 매우 높아지게 된다.


[그림 1] 인터럽트의 개념도


 AVR에서 처리하는 인터럽트는 크게 외부 인터럽트와 내부 인터럽트로 나눌 수 있다. 외부 인터럽트는 용어 그대로 외부에서 발생하는 신호이고 내부 인터럽트는 내장된 주변 장치(타이머/카운터, ADC 등등)에서 발생하는 것이다.



Posted by 살레시오
,

 7세그먼트는 숫자나 시간을 표시하는데 사용되며 우리 주변에서 흔히 볼 수 있는 부품이다. 외관은 아래와 같다.


[그림 1] 7세그먼트의 외관

구동원리는 LED 어레이와 동일하고 도트까지 포함해서 총 8개의 LED가 숫자를 표시하기 위한 레이아웃으로 배치되어 있다. 각각의 LED의 위치 및 명칭과 구조는 다음 [그림 2]와 같다.

[그림 2] 7세그먼트의 구조와 각 LED의 명칭


7세그먼트는 애노드 공통형(anode common type)과 캐소드 공통형(cathod common type)으로 나뉘는데 이는 LED들을 어디에서 결선했는지에 따라서 달라지며 각각에 대한 내부 회로도는 다음 [그림 3]과 같다.


[그림 3] 7세그먼트의 내부 회로도


ATmega8(A)을 이용한 7세그먼트 실험을 위한 회로도는 [그림 4]와 같다. 이것을 구동하기 위해서는 DDRD6='0', PORTD6='1'로 설정해서 GND에 연결된 트랜지스터를 ON시켜야 하여 매크로 Turn7SegOn에 이 동작을 수행하도록 정의하였다. 포트핀의 방향 설정은 초기화 함수 InitAM8()에서 수행한다.

[그림 4] 실험 키트의 7세그먼트 회로도


예를 들어서 숫자 1을 표시 하는 경우는 아래와 같이 LED B와 C를 켜면 된다.

[그림 5] 숫자 1을 표시할 경우


표시 숫자에 따른 전체 데이터 값들은 다음 그림과 같다.


[표 1] 7세그먼트의 숫자 표시 데이터


숫자 0부터 9까지 순차적으로 표시하기

 가장 간단한 실험인 숫자 0부터 9까지 순차적으로 표시하기를 수행한다. <표 5.4.1>에 적힌 데이터를 포트B에 순서대로 내보내기만 하면 된다.


#define F_CPU 16000000
#include <avr/io.h>
#include <util/delay.h>
#include "Am8USBasp.h"
uchar nums[10] =
{0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f };
int main(void) {
   uchar byNumId;
   InitAM8();
   while(1) {
       for (byNumId=0; byNumId<10; byNumId++) {
           SEG(nums[byNumId]);
           _delay_ms(500);
       }
   }
}


숫자 데이터를 저장하기 위해서 배열을 사용했다. main()함수에서 하는 일은 이 배열의 데이터를 시차를 두고 내보는 일을 무한히 반복하는 것이다.



Posted by 살레시오
,

 여기에서는 이전 포스트에 이어서 포트를 이용한 LED 실습을 진행한다.

LED실험 2

 두 번째 LED실험도 앞의 실험과 비슷하다. 이번에는 다음과 같은 순서로 1→2→3→4→1→2→3→4→1→… 계속 시차를 두고 반복하는 실험이다.


[표 1] 두 번째 LED 실험

순서

LED상태

이진수

16진수

1

●●●○○●●●

0b00011000

0x18

2

●●○●●○●●

0b00100100

0x24

3

●○●●●●○●

0b01000010

0x42

4

○●●●●●●○

0b10000001

0x81


프로그램 예는 다음과 같다.


#define F_CPU 16000000

#include <avr/io.h>

#include <util/delay.h>

#include "Am8USBasp.h"

int main(void) {

   uchar ucA, ucaRelay[4] = {0x18, 0x24, 0x42, 0x81};

   InitAM8();

   while(1) {

       for (ucA=0; ucA<4; ucA++) {

           LED(ucaRelay[ucA]);

           _delay_ms(200);

       }

   }

}


이 예를 보면 typedef 명령어로 unsigned char형은 byte형으로 재정의하였으며 앞으로도 이것은 계속 사용할 것이다. 그리고 LED로 순차적으로 내보낼 데이터는 배열로 처리하였음을 눈여겨 보기 바란다.


LED실험 3

 다음 [표 2]에 설명한 바와 같이 1→2→ … →13→14→1→2→ … 의 순서로 14가지 패턴을 반복하면 마치 LED한개가 좌우로 왕복하는 듯이 보이는 예제이다. 이 세 번째 LED실험은 앞의 것들과 달리 조금 복잡해 보인다.


[표 2] 세 번째 LED 실험

순서

LED상태

이진수

16진수

1

●●●●●●●○

0b00000001

0x01

2

●●●●●●○●

0b00000010

0x02

3

●●●●●○●●

0b00000100

0x04

4

●●●●○●●●

0b00001000

0x08

5

●●●○●●●●

0b00010000

0x10

6

●●○●●●●●

0b00100000

0x20

7

●○●●●●●●

0b01000000

0x40

8

○●●●●●●●

0b10000000

0x80

9

●○●●●●●●

0b01000000

0x40

10

●●○●●●●●

0b00100000

0x20

11

●●●○●●●●

0b00010000

0x10

12

●●●●○●●●

0b00001000

0x08

13

●●●●●○●●

0b00000100

0x04

14

●●●●●●○●

0b00000010

0x02


 이번 프로그램을 이전 실험과 같이 14개의 데이터를 포트에 순차적으로 내보내는 식으로 프로그램을 작성할 수도 있으나 그렇게 하면 너무 비효율적인 프로그램이 된다. 그래서 비트이동 연산자와 반복문을 이용하면 좀 더 간단하게 프로그램이 가능하다. 프로그램 예는 다음과 같다.


#define F_CPU 16000000

#include <avr/io.h>

#include <util/delay.h>

#include "Am8USBasp.h"

typedef enum DIR {UP, DOWN} EDIR;

int main(void) {

   uchar ucLed = 1;

   EDIR eDir = UP;

   InitAM8();

   while(1) {

       if (eDir == UP) {

           ucLed <<= 1;

           if (ucLed == 0x80)

               eDir = DOWN;

       } else { // if (eDir == DOWN)

           ucLed >>= 1;

           if (ucLed == 0x01)

           eDir = UP;

       }

       LED(ucLed);

       _delay_ms(200);

   }

}


LED실험 4 : LED탑 쌓기

 마지막으로 아래와 같은 패턴을 보여주는 프로그램을 작성해보자. 벽돌로 하나씩 탑을 쌓는 것처럼 보여서 ‘LED 탑 쌓기’실험이라고 이름을 붙여 보았다.


[표 3] 네 번째 LED 실험

순서

LED상태

이진수

16진수

1

○●●●●●●●

0b01111111

0x7F

2

●○●●●●●●

0b10111111

0xBF

3

●●○●●●●●

0b11011111

0xDF

4

●●●○●●●●

0b11101111

0xEF

5

●●●●○●●●

0b11110111

0xF7

6

●●●●●○●●

0b11111101

0xFB

7

●●●●●●○●

0b11111110

0xFD

8

●●●●●●●○

0b11111110

0xFE

9

○●●●●●●○

0b01111111

10

●○●●●●●○

0b10111111

11

●●○●●●●○

0b11011111

12

●●●○●●●○

0b11101111

13

●●●●○●●○

0b11110111

14

●●●●●○●○

0b11111101

15

●●●●●●○○

0b11111110

16

○●●●●●○○

0b01111111

17

●○●●●●○○

0b10111111

18

●●○●●●○○

0b11011111

19

●●●○●●○○

0b11101111

20

●●●●○●○○

0b11110111

21

●●●●●○○○

0b11111101

22

○●●●●○○○

0b11111110


이 실험은 이제 포트에 데이터를 순차적으로 내보내는 식으로는 프로그램이 거의 불가능하다. 이번에도 역시 비트이동연산자와 반복문을 조합하여 프로그램을 작성할 수 있다.


#define F_CPU 16000000

#include <avr/io.h>

#include <util/delay.h>

#include "Am8USBasp.h"

int main(void) {

  uchar ucaLEDTower[8] = {0x00, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F };

   uchar ucled, uck, ucl;

   InitAM8();

   while (1) {

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

           ucled = 0x80;

           for (ucl=0;ucl<8-uck;ucl++) {

               LED( ucled | ucaLEDTower[uck] );

               _delay_ms(200);

               ucled >>= 1;

           }

       }

   }

}


위 프로그램은 길이는 짧지만 한 줄씩 읽어가면서 분석해 볼 여지가 있다. 다중 반복문을 사용하고 있는데 내부 반복문에서는 LED가 위에서 아래로 한 칸씩 떨어지는 것을 비트쉬프트 연산자로 처리하고 있으며 이 때 PORTB로 내보내는 데이터를 배열 byaLEDTower[]과 OR연산을 하여 마치 벽돌이 쌓여 있는 듯한 효과를 내는 것이다. 배열 byaLEDTower[]은 벽돌이 쌓인 모양의 데이터를 가지고 있다. 바깥 반복문에서는 떨어지는 루틴을 8번 반복하게끔 하는데 떨어지는 곳의 높이를 점차로 높인다.


 이번 포스트에서는 LED를 이용하여 간단한 예제들을 살펴보았다. 독자가 C언어의 문법을 잘 숙지하고 있다면 어떻게 동작이 되는지 쉽게 분석해 볼 수 있을 것이다.




Posted by 살레시오
,

1장 마이크로프로세서 개요
    1.1 마이크로프로세서란 개요
    1.2 마이크로프로세서 분류
    1.3 디지털 시스템의 메모리 분류 
    1.4 마이크로컨트롤러의 I/O 장치들
    1.5 디지털 신호를 표시하기 위한 2진수와 16진수

2장 AVR 개요
    2.1 AVR 마이크로컨트롤러 개요
    2.2 ATmega8(A) 소개
    2.3 ATmega8(A)의 세부적인 특징
    2.4 ATmega8(A)의 메모리 구조
    2.5 락 비트(lock bit)와 퓨즈 바이트(fuse byte)
    2.6 전원과 리셋(reset) 회로
    2.7 클럭 소스 (clock source)
    2.8 개발 환경 : atmel studio 소개
    2.9 오픈소스 다운로더 USBasp / USBaspLoaser
    2.A AVR툴체인의 자료형
    2.B AVR의 플래시롬에 데이터 읽고 쓰기
    2.C AVR의 EEPROM에 데이터 읽고 쓰기
    2.B 기본적인 라이브러리 함수 요약

3장 포트(port)를 이용한 기초 실험
    3.1 포트 개요
    3.2 LED를 이용한 포트 실험 (Part 1)
    3.3 헤더 파일 "Am8USBasp.h"의 구조
    3.3 
LED를 이용한 포트 실험 (Part 2)
    3.5 7세그먼트를 이용한 포트실험 (Part 1)
    3.6 
7세그먼트를 이용한 포트실험 (Part 2)

4장 인터럽트(interrupt)
    4.1 인터럽트 개요
    4.2 인터럽트 프로그래밍
    4.3 외부 인터럽트 설정
    4.4 외부 인터럽트와 스위치 실험 1
    4.5

5장 타이머/카운터
    5.1 타이머/카운터 개요와 T/C0 소개
    5.3 T/C0 의 레지스터와 프리스케일러
    5.3 T/C0를 이용한 실험

    5.4 T/C2 (8비트 타이머/카운터) 개요 및 기능
    5.5 T/C2의 레지스터와 프리스케일러
    5.6 T/C2의 정상 모드와 CTC 모드
    5.7 T/C2의 PWM 모드

    5.8 TC1 (16비트 타이머/카운터) 개요 및 기능
    5.9

6장 아날로그-디지털 변환(ADC)
    6.1 ADC 개요
    6.2 ADC 관련 레지스터들
    6.3 ADC 수행 절차
    6.4 CdS 광센서를 이용한 실습 예제
    6.5

'주제별 글목록' 카테고리의 다른 글

심파이(sympy) 강좌 목록  (0) 2015.06.03
파이썬(python) 기초 강좌 목록  (1) 2015.05.23
아두이노 강좌 목차  (3) 2015.05.17
리눅스(Linux) 강좌 목차  (2) 2015.05.16
C++ 강의 글 목록 (목차)  (2) 2015.04.21
Posted by 살레시오
,