GPIO
Pin 배치도
ATmega128은 다음과 같은 핀 배치도를 가진다. GPIO란 General Purpose Input/Output의 약자로서 한국어로 풀어쓰면, '다용도 입출력 포트' 라고 한다. (한국어로의 번역이 더 어렵다고 생각된다.)
칩에 달려있는 '물리적인 Pin'은 여러 용도로 쓰일 수 있다는 이야기이다.
만약 한 Pin을 General Purpose로 쓸 수 없다면(입력용과 출력용으로 나누어 놓았다고 생각하면), 물리적인 핀이 지금 보는 것에 2배는 되어야 할 것이다. 그 이외에도 칩마다 다양한 특수기능을 지원한다. 위의 Pin 배치도에서 찾아보면, 인터럽트(INT), 아날로그 컨버터(ADC), Usart 통신핀(Tx,Rx)핀 등을 볼 수 있다.
그중 기본이 되는 데이터(신호)를 입/출력 하는 레지스터에 대해서 알아보자.
DDRx
레지스터는 데이터의 입출력 '방향' 지정해주는 레지스터이다. 물리적인 pin의 방향을 정해주는 것이다.
조금더 쉽게 설명하자면, 출입문의 방향을 설정해주는 것이라고 생각하면 쉽다.출입문이 바깥쪽으로 열린다면, 해당 출입문은 내보내는 것만 가능한 '출력'으로 쓰겠다는 것을 의미한다.
출입문이 안쪽으로 열린다면, 해당 출입문은 들어오는 것만 가능한 '입력'으로 쓰겠다는 것을 의미한다.
PORTx
해당 레지스터는 출력과 관련된 레지스터이다. DDRx레지스터를 통해 데이터의 방향이 올바르게 설정되었고, PORTx 레지스터가 set 된다면, 실제 신호가 출력 될것이다.
PINx
해당 레지스터는 입력과 관련된 레지스터이다. DDRx레지스터를 통해 데이터의 방향이 올바르게 설정되었다면, PINx에는 현재 들어온 신호의 상태가 담겨 있다.
위의 표는 물리적인 핀을 쓰는 방법에 대한 설명인데, 일반적인 입/출력 방법, 풀업시키는 방법 등이 나와 있다.
#define F_CPU 16000000
#include <avr/io.h>
#include <util/delay.h>
void MI_GPIO_config()
{
DDRF = 0x00; //입력 - SW 모듈
DDRA = 0xff; //출력 - LED 모듈
DDRG = 0x03; // 0,1 보드 LED, 2,3 보드 SW
}
int main(void)
{
MI_GPIO_config();
while (1)
{
PORTA = PINF;
}
}
위의 코드는 스위치로 부터, 입력받은 신호를 LED 모듈을 통하여 출력 하는 것이다.
#define F_CPU 16000000
#include <avr/io.h>
#include <util/delay.h>
void MI_GPIO_config()
{
DDRF = 0x00; //입력 - SW 모듈
DDRA = 0xff; //출력 - LED 모듈
DDRG = 0x03; // 0,1 보드 LED, 2,3 보드 SW
}
int main(void)
{
MI_GPIO_config();
char current_State_F = 0;
while (1)
{
current_State_F = PINF;
if(PINF == 0x01) //What if this function acquire highest priority
{
PORTA = 0xff;
_delay_ms(500);
PORTA = 0x00;
_delay_ms(500);
}
if(PINF == 0x02)
{
PORTA = 0xF0;
_delay_ms(500);
PORTA = 0x0F;
_delay_ms(500);
}
if(PINF == 0x04)
{
PORTA = 0xaa;
_delay_ms(500);
PORTA = 0x00;
_delay_ms(500);
}
}
}
위 같이 1,2,3번 스위치가 있을때, 각 스위치(each 'if' statement)의 개별 수행시간이 1초 정도 걸리게 된다. 이때 다른 스위치를 누를경우, 입력이 무시되게 되어 있다.
※ _delay_ms( ) 함수를 잠깐 보자 F_CPU(보드 실제 클럭)에 의존해 시간을 만드는 것을 볼 수 있다. 타이머와 같은 기능을 이용하는 것이 아니라, 반복문(while)문을 통해 시간을 지연시키는 경우이다.
void _delay_ms(double __ms)
{
...
uint16_t __ticks;
__tmp = ((F_CPU) / 4e3) * __ms;
if (__tmp < 1.0)
__ticks = 1;
else if (__tmp > 65535)
{
// __ticks = requested delay in 1/10 ms
__ticks = (uint16_t) (__ms * 10.0);
while(__ticks)
{
// wait 1/10 ms
_delay_loop_2(((F_CPU) / 4e3) / 10);
__ticks --;
}
return;
}
else
__ticks = (uint16_t)__tmp;
_delay_loop_2(__ticks);
...
}
만약, 1번 스위치가 눌리 모든 상황에서 응답하도록 만드려면 어떻게 해야할까? (다음의 Interrupt 섹션으로 가자!)
물리주소 접근
사족을 조금 붙이자면 위의 코드의 PORTx, DDRx는 무엇일까? MCU 모든 기능은 메모리 주소와 밴딩(묶여)되어있다. POTRG를 통해 예시를 들겠다.
_delay_ms(300);
PORTG = 0x03;
_delay_ms(300);
PORTG = 0x00;
_delay_ms(300);
_SFR_MEM8(0x65) = 0x03;
_delay_ms(300);
_SFR_MEM8(0x65) = 0x00;
_delay_ms(100);
_MMIO_BYTE((0x65)) = 0x03;
_delay_ms(100);
_MMIO_BYTE((0x65)) = 0x00;
_delay_ms(300);
(*(volatile unsigned char *)(0x65)) = 0x03;
_delay_ms(300);
(*(volatile unsigned char *)(0x65)) = 0x00;
결국 PORTG는 위와 같이 정의 되어있는 것이다.
Register Summary Section을 참고하면 어떤 물리주소로 접근하면 되는지가 나와있다. 레지스터는 물리주소를 쉽게 기억하기 위한 수단이라고 생각하면, 쉽다. 모든 코드를 포인터 주소로 접근해야한다면, 가독성이 매우 떨어질 것이다.