이번에는 스위치와 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 살레시오
,

 본 포스트에서는 ATmega8A의 포트에 LED어레이를 연결하여 LED를 구동시키는 실습을 진행한다. 실습을 위해서 LED 8개를 PB4:0와 PC4:0에 연결하며 회로도는 다음 그림과 같다. LED는 4비트씩 나누어서 포트B와 포트C에 연결하였다.


[그림 1] LED실습을 위한 회로도


이 회로도를 살펴보면 PD5핀이 스위칭 트랜지스터의 베이스와 연결되어 있음을 알 수 있다. 트랜지스터를 온시키기 위해서는 베이스에 아주 작은 양의 전류를 흘려주어야 하는데 보통 이를 위해서 큰 저항(수십~백kΩ)을 베이스와 포트핀 사이에 연결해야 한다. 하지만 본 실험에서는 외부 저항을 생략하고 그 대신에 ATmega8(A)내부에 내장된 풀업 저항을 이용하여 트랜지스터를 on시키는 방법을 사용하였다. 이를 위해서 DDRD5비트를 입력으로 (‘0’) 설정하며, 트랜지스터를 on시키기 위해서는 PORTD5 비트를 온(‘1’)시켜서 내부 풀업을 연결한다. 그 이후에 PB4:0와 PC4:0에 표시하려는 데이터를 내보내면 LED에 그 데이터가 표시되게 되는 것이다. (PD5핀과 PD6핀을 입력으로 설정해야 함에 주의해야 한다. 이 핀들을 출력으로 설정해 버리면 트랜지스터를 on/off시킬 수 없다.)


 또한, 포트핀의 내부 저항이 25Ω 정도인 것을 고려하면 외부 저항의 용량은 저항은 300Ω 정도가 적당하지만, 본 실습 장치는 USB전원으로 구동되므로 전류를 줄이고 LED가 켜졌음을 확인하는데 문제가 없는 수준에서 포트핀으로 흐르는 전류 값을 최소화하기 위해서 1KΩ의 저항을 연결하였다.

 만약 PB0부터 PB4까지가 모두 동시에 세트(‘1’된다 하더라도 포트B로 흘러나가는 전류의 합은 다음 식과 같이 계산할 수 있다.



여기서 1.7V는 LED양단의 전압 강하 값이다. 이것은 포트B로 흐르는 전류의 합이 100mA 이하여야 된다는 데이터쉬트상의 스펙을 벗어나지 않게 된다. 포트C에 대해서도 같은 계산 결과가 적용될 수 있을 것이다.


 다음과 같이 LED에 관련된 매크로와 함수를 정의한다.


#define sbi(sfr,bit) (_SFR_BYTE(sfr)|=_BV(bit))
#define cbi(sfr,bit) (_SFR_BYTE(sfr)&=~_BV(bit))
typedef unsigned char uchar;
typedef unsigned int uint;
typedef unsigned long ulong;

#define TurnLEDOn ({sbi(PORTD,5);cbi(PORTD,6);})
#define Turn7SegOn ({sbi(PORTD,6);cbi(PORTD,5);})
#define TurnOffAll ({cbi(PORTD,5);cbi(PORTD,6);})
void LED(uchar b) { // Display b into LED
   TurnLEDOn;
   PORTB &= 0xF0;
   PORTC &= 0xF0;
   PORTB |= (b&0x0F);
   PORTC |= (b>>4);
}


 먼저 sbi(sft, bit)와 cbi(sft, bit) 매크로는 sfr 레지스터의 bit번을 세트시키는 매크로이다. 예를 들어 PORTB레지스터의 2번 비트를 세트시키고 싶다면 sbi(PORTB,2) 라고 하면 되고 PORTD 레지스터의 5번 비트를 리셋시키고 싶다면 cbi(PORTD,5) 라고 하면 된다. 또한 unsigned char형을 uchar 로 재정의 했는데 이는 긴 데이터 형의 이름을 짧게 사용하기 위한 것이다. uint 와 ulong 도 같은 이유로 재정의했다.

 LED와 관련된 매크로는 TurnLEDOn, Turn7SegOn, TurnOffAll 세 개가 정의되어 있는데 각각 LED만 켜고, 7세그먼트만 켜고, 둘 다 끄는 동작을 수행하는 매크로이다. 그리고 LED(uchar b)함수가 정의되어 있는데 이 함수에는 LED에 내보낼 데이터를 넘겨주면 된다. LED와 7세그먼트가 포트B의 하위 4비트와 포트C의 하위 4비트에 걸쳐 있기 때문에 LED( )함수의 내부에는 넘겨진 데이터의 하위 4비트를 PORTB4:0에, 그리고 상위 4비트를 PORTC4:0에 복사하는 코드가 작성되어 있다. 이 때 PORTB, PORTC레지스터의 상위 4비트는 그 값을 그대로 유지하도록 하였다.


LED실험 (1)

 첫 번째 실험은 [표 1]과 같이 4개씩 LED를 번갈아서 켜는 실험이다.


[표 1] 첫 번째 LED 실험

순서

LED상태

이진수

16진수

1

○○○○●●●●

0b00001111

0x0F

2

●●●●○○○○

0b11110000

0xF0


위의 표에서 1-2-1-2- 순으로 일정한 시차를 두고 무한히 반복하는 실험이다. 프로그램 예는 다음과 같다.


#define F_CPU 16000000
#include <avr/io.h>
#include <util/delay.h>

typedef unsigned char uchar;
typedef unsigned int uint;
typedef unsigned long ulong;

#define sbi(sfr,bit) (_SFR_BYTE(sfr)|=_BV(bit))
#define cbi(sfr,bit) (_SFR_BYTE(sfr)&=~_BV(bit))

#define TurnLEDOn ({sbi(PORTD,5);cbi(PORTD,6);})
#define Turn7SegOn ({sbi(PORTD,6);cbi(PORTD,5);})
#define TurnOffAll ({cbi(PORTD,5);cbi(PORTD,6);})
void LED(uchar b) { // Display b into LED
   TurnLEDOn;
   PORTB &= 0xF0;
   PORTC &= 0xF0;
   PORTB |= (b&0x0F);
   PORTC |= (b>>4);
}
int main(void) {
   InitAM8();
   uchar ucLED = 0x0F;
   while(1) {
       LED(ucLED);
       ucLED = ~ucLED;
       _delay_ms(500);
   }
}


이 프로그램을 보면 main()함수 안에서 while(1){ } 반복문에 의해서 무한루프에 빠지게 프로그램이 되어 있다. 일반적으로 PC상에서는 운영체제 (Operation System, OS로 줄여 표기하고 윈도우, 리눅스 등이 있다.)가 응용프로그램을 실행시키고 응용프로그램이 종료가 되면 다시 OS로 되돌아가지만 uC는 OS가 없기 때문에 응용프로그램 안에서 프로그램 실행이 계속 머물러야 하므로 무한 루프가 이용된다.


 전술한 바와 같이 LED를 켜는 코드와 7세그먼트를 켜는 코드를 매크로 TurnLEDOn, Turn7SegOn 으로 각각 정의해 놓고 있어서 LED를 구동시키고 싶을 때는 TurnLEDOn이라는 매크로를 사용하면 된다. 이렇게 매크로를 정의해 놓은 이유는 사용의 편의성 때문이기도 하지만 프로그램의 가독성 때문에도 그렇다. 예를 들어서 main()함수 중간에


sbi(PORTD,5);
cbi(PORTD,6);


라고 작성되어 있다고 가정해보자. 다른 사람이 이 프로그램을 읽는다면 한 눈에 이 두 줄이 무슨 역할을 하는지는 알기 힘들 것이다. 하지만


TurnLEDOn;


이라고 작성하면 이것은 이전의 것과 똑같은 코드를 생성하지만 (전처리기에 의해서 이것은 ({sbi(PORTD,5);cbi(PORTD,6);})로 컴파일하기 전에 치환된다.) 한 눈에 LED를 켜는 코드라고 알아볼 수 있을 것이다.



Posted by 살레시오
,

 포트(port)란 uC가 외부 기기와의 데이터를 주고받는 통로를 말한다. 입력되는 신호를 받고 출력 신호를 내보낼 수 있는 핀으로서 AVR에서는 8개씩 묶어서 포트A, 포트B, 포트C, … 와 같은 식으로 이름이 붙어 있는 것이 일반적이다. 예를 들어 포트D는 8개의 핀으로 구성되며 각 핀은 PD0, PD1, … PD7과 같이 이름이 붙어 있다. 특히 AVR의 경우는 입/출력전류 드라이브 능력이 40mA정도로 높은 편이며 내부에 풀업 저항 (20k~50k)을 내장하여 선택적으로 사용할 수 있으므로 스위치나 센서 등을 연결하는 경우 회로를 간략화할 수 있다.  포트핀의 구조도는 아래와 같다.


[그림 1] avr의 포트 핀 회로도


 풀업(pull-up) 저항이란 디지털 회로에서 논리적으로 H레벨을 유지하기 위해서 포트핀과 Vcc를 연결하는 저항을 말한다. 보통은 uC외부에서 연결해야 하지만 AVR은 모든 포트핀에 풀업저항을 내장하고 있어서 SFR을 적절히 설정하면 선택적으로 연결할 수 있다.


포트와 관련된 레지스터

 AVR은 각각의 포트에 3개의 관련 I/O레지스터를 가지는데 다음과 같다.

       ① DDRX (Data Direction Register) 레지스터

       ② PORTX (Data Register) 레지스터

       ③ PINX (Port Input Pin Address) 레지스터

모두 8비트 레지스터이고 ATmega8(A)의 경우는 X는 B, C, 혹은 D가 된다. 먼저 포트가 입력으로 쓰일지 출력으로 쓰일지 방향을 정해야 하는데

  • DDRX의 해당 비트가 '1' 이면 출력,

  • DDRX의 해당 비트가 '0'이면 입력

으로 지정된다. 그리고

  • 출력인 경우 PORTX 에 출력값을 쓰고,

  • 입력인 경우에는 PINX에서 값을 읽어들인다.

입력값을 읽는 레지스터와 출력값을 내보내는 레지스터가 다르다는 것에 유의해야 한다. 또한 DDRx의 설정과 무관하게 PINx레지스터를 이용하여 해당 핀의 전압 값을 읽어 들일 수 있다. 만약 포트핀이 출력으로 설정되었다면 PINXn 비트는 PORTXn비트의 값과 같아지고 입력으로 설정되었다면 PINXn비트는 핀에 인가되는 전압값에 의해서 그 값이 결정된다.


 또 한 가지 알아야 할 것은 포트핀이 입력으로 설정된 경우 PORTX레지스터는 풀업 저항을 연결할 것인지 아닌지를 결정한다는 것이다. 이를 아래 [표 1]에 도시하였다. PUD비트는 MCUCR의 b4이다.


[표 1] portx 레지스터의 설정에 따른 동작

DDRXn

PORTXn

PUD

I/O

풀업저항

비고

0

0

×

입력

연결 안 됨

3스테이크(하이임피던스)

0

1

0

입력

연결됨

내부에서 풀업

1

0

×

출력

연결 안 됨

'0' 출력 (sink)

1

1

×

출력

연결 안 됨

'1' 출력 (source)


PORTX 레지스터는 비트별로 그 값을 각각 지정해 줄 수 있다. 즉 같은 포트라도 어떤 핀은 입력으로 다른 핀은 출력으로 사용할 수 있다. 다음은 ATmega8(A)의 포트에 관련된 레지스터를 정리해 놓은 것이다.



예를 들어서 ATmega8(A)의 2번 핀은 PD0/RxD 두 가지 기능이 있다. 이 핀을 포트핀(즉 PD0)으로 사용하고 싶다면 DDRD0 비트를 설정하여 입력/출력 방향을 결정해야 한다. 그 다음 출력일 경우 PORTD0 비트를 이용하여 출력할 값을 내보내면 되고 입력일 경우 PIND0 비트값을 읽어들여서 핀에 인가되는 전압값을 읽어들이면 된다.


포트핀의 내부 저항값과 등가 회로

 AVR 포트핀이 출력으로 쓰일 때 데이터쉬트에서 발췌한 전압-전류 곡선이 [그림 1]에 도시되어 있다.

[그림 2] 포트 핀의 전압-전류 곡선 (Vcc=5V) : (a) 출력=1 (b) 출력 = 0


이 그림에서 직선 (엄밀히 말하면 곡선이지만 곡률이 거의 0이므로 직선이라고 가정한다)의 기울기가 바로 저항값이 되며 이 직선의 기울기가 실온일 경우 약 25 정도 된다는 것은 쉽게 알 수 있다. 이는 포트핀의 내부저항이 약 25Ω정도 된다는 것을 의미하며 이것을 근거로 포트핀의 테브낭 등가회로를 작성해 보면 [그림 2]와 같다.


[그림 3] (a) 출력 = 1 인 경우 (b) 출력 = 0 인 경우


포트핀의 내부저항이 약 25Ω이라는 사실을 아는 것은 매우 중요한데, 그 이유는 이 등가회로를 이용하면 주변 회로의 전압-전류를 정량적으로 계산할 수 있기 때문이다.


 예를 들면 포트핀에 센서를 연결했을 경우 센서의 데이터쉬트에서 필요한 정보를 얻은 후에 실제 전압-전류값을 정량적으로 계산할 수 있다.



Posted by 살레시오
,

 대부분의 AVR은 EEPROM을 내장하고 있으며, 비휘발성 메모리이기 때문에 시스템 파라메터나 설정 데이터 등 전원이 인가되지 않아도 유지해야 할 데이터들을 저장하는데 많이 사용된다. 비록 비휘발성이긴 하나 수명에는 제한이 있는데 datasheet에 의하면 읽는 동작은 무제한 가능하지만 쓰기는 약 100,000번 정도만 가능하다고 한다. 따라서 EEPROM에 많이 의존하는 계통을 개발할 경우에는 이러한 점을 고려해야 한다.


 AVR은 이러한 내장 EEPROM에 데이터를 읽고 쓰기 위해서 특정한 IOCR을 이용하여 그 읽고 쓰는 동작을 제어하며 WinAVR은 이러한 동작을 수행하는 편리한 함수들을 제공한다. 이 함수들은 <avr/eeprom.h>에 정의되어 있으므로 EEPROM에 접근하기 위해서는 이 헤더화일을 반드시 인클루드시켜야 한다.


#include <avr/eeprom.h>


매뉴얼에 제시된 함수들을 모두 리스트업하면 다음과 같다.


uint8_t eeprom_read_byte (const uint8_t *__p) __ATTR_PURE__
uint16_t eeprom_read_word (const uint16_t *__p) __ATTR_PURE__
uint32_t eeprom_read_dword (const uint32_t *__p) __ATTR_PURE__
float eeprom_read_float (const float *__p) __ATTR_PURE__
void eeprom_read_block (void *__dst, const void *__src, size_t __n)
void eeprom_update_byte (uint8_t *__p, uint8_t __value)
void eeprom_update_word (uint16_t *__p, uint16_t __value)
void eeprom_update_dword (uint32_t *__p, uint32_t __value)
void eeprom_update_float (float *__p, float __value)
void eeprom_update_block (const void *__src, void *__dst, size_t __n)
void eeprom_write_byte (uint8_t *__p, uint8_t __value)
void eeprom_write_word (uint16_t *__p, uint16_t __value)
void eeprom_write_dword (uint32_t *__p, uint32_t __value)
void eeprom_write_float (float *__p, float __value)
void eeprom_write_block (const void *__src, void *__dst, size_t __n)

이 함수들을 분류하여 요약하면 다음 표와 같다.


[표 1] EEPROM에 접근하기 위한 함수들 요약

데이터

읽기

갱신

쓰기

byte

eeprom_read_byte()

eeprom_update_byte()

eeprom_write_byte()

word

eeprom_read_word()

eeprom_update_word()

eeprom_write_word()

dword

eeprom_read_dword()

eeprom_update_dword()

eeprom_write_dword()

float

eeprom_read_float()

eeprom_update_float()

eeprom_write_float()

block

eeprom_read_block()

eeprom_update_block()

eeprom_write_block()


이 표에서 보면 크게 세 가지 부류의 함수들이 정의되어 있는 것을 알 수 있는데 읽기/갱신/쓰기이다. 읽기함수는 EEPROM에서 데이터를 읽어오는 동작을 수행하며 전술한 바와 같이 읽는 동작과 EEPROM의 수명과는 무관하다. 쓰기 함수는 쓰려고 하는 그 장소의 데이터와는 상관없이 무조건 주어진 데이터를 쓰는 동작을 수행한다. 이에 반해서 갱신 함수는 쓰려는 데이터와 현재의 데이터가 서로 다른 경우에만 쓰기를 수행한다. 따라서 EEPROM의 수명을 고려하면 쓰기 함수보다는 갱신함수가 무조건 유리하기 때문에 새롭게 개발하는 경우라면 쓰기함수 대신 갱신함수를 사용해야 한다. 쓰기 함수는 과거에 개발된 프로그램과의 호환을 위하여 남겨둔 것이다.


 만약 46번지에서 한 바이트 값을 읽어서 변수에 저장하려면 다음과 같이 하면 된다. 데이터가 word, dword, float인 경우도 유사하게 데이터를 읽어낼 수 있다.


#include <avr/eeprom.h>
int main(void) {
   uint8_t byA;
   byA = eeprom_read_byte((const uint8_t*)46);
}


갱신하는 동작은 주소 외에 기록할 데이터도 입력해야 한다.


#include <avr/eeprom.h>
int main(void) {
   uint8_t byA = 0xEA;
   eeprom_update_byte((uint8_t*)46, byA);
}


만약 워드 데이터를 쓰는 경우라면 다음과 같을 것이다.


#include <avr/eeprom.h>
int main(void) {
   eeprom_update_word((uint16_t*)46, 0x1234);
}


dword나 float형 변수값을 갱신하는 경우에도 위의 경우와 유사하다.



Posted by 살레시오
,