LED 제어 3
DDR, PORT, PIN 레지스터를 이용하여 초록색 LED와 빨간색 LED를 교대로 점멸하는 코드를 만들어 보았다. 그리고 실제로 보드에서 동작되는것도 확인해 보았다.
그러나 C언어를 조금이라도 배워본 경험이 있다면, 코드안에 있는 DDRB, PORTB, PINB라는것이 무엇인지, 갑자기 어디서 나왔는지 궁금할 것이다.
이번 글에서는 이 부분에 대한 설명과 어떻게 이들 레지스터에 접근할 수 있는 지 설명하겠다.
위의 그림은 사용하고 있는 AVR의 내부 블럭도이다. 우리가 만든 LED 제어 프로그램은 AVR CPU박스의 왼쪽 위에 연결된 Flash에 저장되어 있다. AVR 칩에 전원이 들어가면 AVR CPU는 Flash에서 수행하여야 할 명령어를 하나씩 읽어 들여 프로그램을 수행한다.
앞에서 설명했던 코드에서 사용한 DDRB, PORTB, PINB 레지스터는 PORT B 박스안에 포함되어 있다. 그럼, AVR CPU는 어떤 방법을 통해서 PORT B의 레지스터 값을 변경시킬 수 있을까?
CPU가 입출력(I/O) 레지스터에 값을 쓰거나, 레지스터에서 값을 읽어 오기 위해서 크게 두가지 방법중 하나를 이용한다. 하나는 memory mapped I/O 방식이고, 다른 하나는 port mapped I/O 방식이다.
Memory mapped I/O 방식은 모든 레지스터에 주소를 할당하여 그 주소를 이용하여 레지스터에 접근할 수 있는 방식이다. 이에 반해 port mapped I/O 방식은 I/O 레지스터에 주소 대신 번호를 할당해서 CPU가 접근 할 수 있도록 하는 방식이다.
대부분의 임베디드 CPU는 memory mapped I/O 방식을 사용한다. Port mapped I/O 방식을 사용하는 대표적인 CPU는 바로 PC에서 사용하는 인텔의 x86계열이다. 예를 들어 시리얼 통신을 위한 COM1 포트는 포트번호 0x3F8~0x3FF를 이용한다. x86 CPU에서 I/O 레지스터에 접근하기 위해서 주소를 사용할 수 없으므로 inp, outp라는 명령어를 사용한다.
AVR은 memory mapped I/O 방식을 사용한다. 따라서 데이터쉬트에 DDRB, PORTB, PINB의 주소가 설명되어 있을것이므로 데이터쉬트를 잘 찾아보면 된다.
챕터 36. Register Summary에 AVR에서 사용할 수 있는 모든 I/O 레지스터의 주소와 각 bit들의 기능에 대해 간단히 표로 설명하고 있다. 그 표안에서 DDRB, PORTB, PINB의 주소를 찾으면 된다.
위의 표에 나와 있는것처럼 PINB는 0x03, DDRB는 0x04, PORTB는 0x05 번지가 할당되어 있는 것을 알 수 있다. 각 레지스터의 세부 동작 설명과 각 bit의 의미에 대해서는 블럭별 설명에서 다루고 있다. PORT B 블럭에 있는 DDRB, PORTB 레지스터에 대해서는 이미 앞의 글에서 설명하였다.
한편, Atmel Studio에서도 코드 화면에서 DDRB, PORTB, PINB에 마우스를 클릭하면 Definition 창에서 아래 그림처럼 각 레지스터의 주소를 볼 수 있다.
작성한 C코드를 컴파일러가 어셈블로 변환한 내용을 분석해 봐도 이러한 내용을 확인할 수 있다.
프로젝트 폴더에 생성된 Debug폴더 안에서 led.lss 파일을 열어보자.
lss 파일은 실행파일을 기계어와 어셈블코드로 변환해 놓은 파일이다. led.lss에서 아래와 같은 내용을 볼 수 있을 것이다.
00000080 <main>:
#define GREEN_LED _BV(PB0)
#define RED_LED _BV(PB1)
int main(void)
{
DDRB = (GREEN_LED|RED_LED);
80: 83 e0 ldi r24, 0x03 ; 3
82: 84 b9 out 0x04, r24 ; 4
PORTB= (GREEN_LED|RED_LED);
84: 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);
86: 2f e9 ldi r18, 0x9F ; 159
88: 36 e8 ldi r19, 0x86 ; 134
8a: 91 e0 ldi r25, 0x01 ; 1
8c: 21 50 subi r18, 0x01 ; 1
8e: 30 40 sbci r19, 0x00 ; 0
90: 90 40 sbci r25, 0x00 ; 0
92: e1 f7 brne .-8 ; 0x8c <main+0xc>
94: 00 c0 rjmp .+0 ; 0x96 <main+0x16>
96: 00 00 nop
while (1)
{
_delay_ms(500);
PINB = (GREEN_LED|RED_LED);
98: 83 b9 out 0x03, r24 ; 3
}
9a: f5 cf rjmp .-22 ; 0x86 <main+0x6>
9번줄과 11번 줄을 보면 0x04와 0x05에 r24에 있는 값을 내보내는 것을 볼 수 있다. 마찬가지로 32번줄에도 0x03에 r24의 값을 내 보내는 명령어가 있다. 8번줄에서 r24에 3을 넣는 명령어가 실행되므로 위의 모든 레지스터에 3을 쓰는 동작을 수행하게 된다.
0x03, 0x04, 0x05가 PINB, DDRB, PORTB 레지스터의 주소인 것이다.
이를 통해서 프로그램으로 어떤 I/O를 제어하고자 할 때에는 해당 블럭의 레지스터를 주소를 통해서 접근할 수 있다는 것을 알 수 있다. 문제는 어떻게 하면 특정 주소에 원하는 값을 쓸수 있는가 이겠다.
이러한 필요에 의해 C언어는 특정 주소에 어떤 값을 쓰거나 읽어 오기 위해서 포인터를 사용한다.
아마 C언어를 막 배우기 시작 했거나, PC에서 수행되는 프로그램만 짜본 프로그래머라면 포인터에 대한 개념을 이해하기 힘들었을 것이다. 도대체 포인터가 무엇이고 왜 사용하여야 하는지 궁금해 했을 것이다.
그러나 임베디드 시스템 프로그램을 해보면 포인터를 사용하지 않고는 단 하나의 기능도 구현할 수 없다는 것을 금방 이해하고 받아들이게 된다. 포인터를 사용하지 않고는 레지스터에 접근할 방법이 없기 때문이다.
다음 글에서는 AVR에서만 사용할 수 있는 방법이 아닌, 일반적인 포인터를 이용하여 LED를 제어하는 방법에 대해서 알아 보겠다.