티스토리 뷰
지금까지 LED제어, 푸쉬버튼 감지 그리고 DS18B20과 통신하기 위하여 거의 모든 코드에서 delay함수를 사용하여 일정 시간동안 프로그램의 진행을 잠시 멈추는 기능을 수행하였다. 임베디드 시스템에서는 이와 같이 상당히 많은 부분에서 시간과 관련된 코드가 들어가게 된다.
_delay_ms()나 _delay_us() 함수는 거의 정확한 시간동안 시간지연 기능을 수행해 준다. 그러나 이 함수들은 치명적인 단점이 있다. 특정 시간동안 delay를 주기 위하여 함수 인자로 시간지연값을 넣어 주는데 이 값들은 반드시 상수이어야만 한다는 것이다.
LED 제어 1에서 만든 코드를 가지고 이 말이 무슨뜻인지 설명해 보도록 하겠다.
int main(void)
{
DDRB = 0x03;
PORTB= 0x03;
while (1)
{
_delay_ms(500);
PINB = 0x03;
}
}
위의 코드가 두개의 LED를 교대로 점멸하기 위하여 사용했던 코드이다. 1초 주기로 한번씩 점멸하기 위하여 8번 라인처럼 _delay_ms() 안에 500이라는 상수값을 넣어주었다. 그런데 이렇게 일정한 주기로 LED를 깜빡이는게 너무 단조롭게 느껴져 시간 주기를 바꾸고 싶어서 다음과 같은 코드처럼 수정하였다.
int main(void)
{
int i;
DDRB = 0x03;
PORTB= 0x03;
while (1)
{
for (i=10; i<=100; i+=10)
{
_delay_ms(i);
PINB = 0x03;
}
}
}
delay값을 10부터 시작하여 100이 될때까지 10msec 단계로 시간 지연값을 증가 시키는 코드이다. 이렇게 코드를 수정한 후 프로그램을 빌드하면 뜻하지 않게 컴파일 에러가 발생한다.

위의 그림과 같이 "컴파일 할때 정수형 상수값을 기대한다"는 에러가 발생하는 것이다.
왜 이런 에러가 발생하는지 그 이유는 컴파일 후 생성된 어셈블 코드를 보면 알 수 있다. _delay_ms(500)이 어떻게 컴파일 되었는지 lss 파일을 열어보면 다음과 같이 변환된 코드를 볼 수 있다.
000000a6 <main>:
#include <avr/io.h>
#include <util/delay.h>
int main(void)
{
DDRB = 0x03;
a6: 83 e0 ldi r24, 0x03 ; 3
a8: 84 b9 out 0x04, r24 ; 4
PORTB= 0x03;
aa: 85 b9 out 0x05, r24 ; 5
#else
//round up by default
__ticks_dc = (uint32_t)(ceil(fabs(__tmp)));
#endif
__builtin_avr_delay_cycles(__ticks_dc);
ac: 2f e9 ldi r18, 0x9F ; 159
ae: 36 e8 ldi r19, 0x86 ; 134
b0: 91 e0 ldi r25, 0x01 ; 1
b2: 21 50 subi r18, 0x01 ; 1
b4: 30 40 sbci r19, 0x00 ; 0
b6: 90 40 sbci r25, 0x00 ; 0
b8: e1 f7 brne .-8 ; 0xb2 <main+0xc>
ba: 00 c0 rjmp .+0 ; 0xbc <main+0x16>
bc: 00 00 nop
while (1)
{
_delay_ms(500);
PINB = 0x03;
be: 83 b9 out 0x03, r24 ; 3
}
c0: f5 cf rjmp .-22 ; 0xac <main+0x6>
C코드와 어셈블코드가 같이 나와 있어서 다소 이해하기 어렵겠지만 자세히 보면 별로 복잡한 코드가 아님을 알 수 있다.
위의 코드에서 18~26에 나와있는 어셈블코드가 _delay_ms(500)에 해당되는 내용이다.
ldi라는 명령을 이용하여 r18,r19,r25에 각각 0x9F,0x86,0x01의 값을 넣어준다. 그 다음에 subi명령으로 r18에서 1을 뺀다. 다음엔 sbci 명령을 이용하여 r19에서 0을 뺀다. sbci는 캐리를 포함하여 빼기를 하는 명령어이다. 앞의 어셈블 코드에서 r18의 값이 0에서 1을 뺄때 캐리가 발생하므로 그 순간에 r19의 값이 1 감소하게 된다. 23번 라인도 앞의 어셈블 코드와 동일하게 r19의 값이 0에서 1을 뺄때 발생한 캐리를 이용하여 r25의 값을 감소 시킨다. r25의 값이 0에서 0xff로 변할때까지 21번 라인으로 돌아가서 계속 1을 빼나가는 코드를 반복하게 된다.
한마디로 말해서 0x01869F에서 1을 빼는 코드를 수행하여 그 값이 0이 될때까지 반복하는것이다. 갑자기 뜬금없이 0x01869F는 어디에서 나타난것일까? 0x01869F를 십진수로 변환하면 99999가 된다. 반복 수행하는데 사용되는 명령어인 subi, sbci는 모두 1cycle의 처리 시간이 필요하다. 그리고 brne는 조건이 참이면 2cycle, 거짓이면 1cycle 소비된다. r18,19,r25의 값이 모두 0이 될때까지는 조건이 참이 되므로 brne는 계속 2cycle의 시간을 소비하게 된다. 결국 21~24라인을 수행하는데 모두 5cycle의 시간이 소비되는 것이다. r18,r19,r25의 값이 모두 0이 되어 brne이 드디어 거짓이 되면 1cycle의 시간을 소비하면서 다음 명령어인 rjmp로 넘어가게 된다. rjmp는 2cycle의 시간을 필요로 한다. 여기서 rjmp .+0을 수행하면 그 다음 명령어인 nop으로 점프한다. 이렇게 하는 이유는 단순히 2cycle을 소비하기 위해서다. nop 명령어는 1cycle의 시간을 소비한다. 18~20 라인에 사용된 ldi 명령어는 1cycle의 시간을 소비한다.
자, 이제 18번 라인에 진입하면서부터 26번 라인에 있는 nop 명령어가 수행 완료되는 cycle을 전부 계산해 보자. 1+1+1+(1+1+1+2) * 99998+(1+1+1+1)+2+1 = 500000이 된다. 현재 AVR이 1MHz의 클럭으로 동작되므로 1cycle이 동작되는 시간은 1usec이 된다. 따라서 500000cycle은 500000usec = 500msec가 되는 것이다. _delay_ms(500)을 수행하기 위하여 정확히 500msec의 시간을 소모하게 되는것이다.
AVR이 1MHz로 동작하므로 F_CPU값을 1000000으로 지정해 주었다. 만약 AVR이 8MHz로 동작되는 환경이라면 F_CPU의 값을 8000000으로 설정해 주어야 한다. F_CPU값을 다른 값으로 변경한 후 다시 컴파일하여 어셈블 코드를 분석해보자.
ac: 2f ef ldi r18, 0xFF ; 255
ae: 34 e3 ldi r19, 0x34 ; 52
b0: 9c e0 ldi r25, 0x0C ; 12
b2: 21 50 subi r18, 0x01 ; 1
b4: 30 40 sbci r19, 0x00 ; 0
b6: 90 40 sbci r25, 0x00 ; 0
b8: e1 f7 brne .-8 ; 0xb2 <main+0xc>
ba: 00 c0 rjmp .+0 ; 0xbc <main+0x16>
bc: 00 00 nop
위의 어셈블 코드는 F_CPU값을 8000000으로 설정하고 난 후 컴파일된 결과이다. 반복회수가 0x0C34FF로 변경된것을 볼 수 있다. 십진수로 변환하면 799999가 된다. 앞에서 설명한대로 실제 _delay_ms(500)가 수행되는 전체 cycle은 4000000이 된다. AVR CPU가 8MHz로 동작되므로 1cycle 수행되는 시간은 1/8 usec이 된다. 따라서 4000000cycle은 500msec가 되는 것이다.
F_CPU의 값을 원래대로 1000000으로 되돌린 다음 _delay_ms(5)로 코드를 수정한 다음 다시 컴파일하여 어셈블 코드를 보면 다음과 같이 나온다.
ac: e1 ee ldi r30, 0xE1 ; 225
ae: f4 e0 ldi r31, 0x04 ; 4
b0: 31 97 sbiw r30, 0x01 ; 1
b2: f1 f7 brne .-4 ; 0xb0 <main+0xa>
b4: 00 c0 rjmp .+0 ; 0xb6 <main+0x10>
b6: 00 00 nop
반복 회수가 0x04E1로 바뀌었고 어셈블코드도 조금 바뀌었다. 위의 코드가 실행되는 cycle을 계산해보면 다음과 같이 된다. 1+1+(2+2)*1248+(2+1)+2+1 = 5000cycle이 된다. 시간으로 변환하면 5msec가 되는 것이다.
F_CPU가 바뀌거나 _delay_ms()에 들어가는 값이 바뀔때마다 코드가 변경되는것을 확인할 수 있었다. 결과적으로 _delay_ms() 함수는 컴파일 되는 시점에 코드가 결정된다는 것을 알수 있다. 따라서 _delay_ms()에 상수가 아닌 변수가 들어가게 되면 코드를 결정할 수 없으므로 컴파일 에러가 발생하게 되는 것이다.
그럼, 코드를 수정하지 않고 가변적으로 delay를 주려면 어떻게 해야 될까?
그 방법은 다음 글에서 설명하도록 하겠다.
이 글에서 설명된 어셈블 코드에 대한 명령어들의 자세한 설명은 "Atmel AVR 8-bit Instruction Set" 문서를 찾아보기 바란다.