티스토리 뷰
푸쉬 버튼 감지 1에서 사용한 예제는 역시 AVR에서만 사용할 수 있는 코드이다. 이를 일반적인 임베디드 프로그램으로 표현해 보도록 하겠다.
#include <stdint.h>
#define PORTB_REG 0x23
#define PUSH_BTN 0x01
#define RED_LED 0x02
struct port
{
uint8_t pin;
uint8_t ddr;
uint8_t port;
};
volatile struct port *const portb = (void*)PORTB_REG;
void setup(void)
{
portb->ddr = RED_LED;
portb->port= (RED_LED | PUSH_BTN);
}
void loop(void)
{
if (portb->pin & PUSH_BTN)
portb->port |= RED_LED;
else
portb->port &= ~RED_LED;
}
int main(void)
{
setup();
while (1)
{
loop();
}
}
위의 코드는 이미 LED제어편에서 다루어 봤던 내용이라 쉽게 이해할 수 있을 것이다. 단지 몇가지 부분에 대해서만 설명하도록 하겠다.
1번줄에 stdint.h 파일을 include 하였다. 이유는 10,11,12번줄에 나와있는 uint8_t에 대한 정의가 stdint.h파일에 있기 때문이다. uint8_t는 unsigned char와 동일한 정의이다. 1바이트 크기의 부호없는 정수형 타입 변수를 선언할 때 사용한다. 2바이트 변수는 uint16_t를 사용하고 4바이트 변수는 uint32_t를 사용하면 프로그램에서 변수형에 대한 정의를 명확히 알 수 있다.
char, short, long 같은 경우는 대부분의 컴파일러가 1바이트, 2바이트, 4바이트 형으로 선언되기는 하지만 CPU나 컴파일러에 따라 다르게 선언될 가능성이 있다. 특히 int형 선언은 CPU에 따라 다르게 설정되는 경우가 상당히 많은 편이므로 프로그램 하기전에 반드시 확인해 두어야 한다. 당장 AVR의 경우에도 int형은 2바이트 크기를 가진다.
이렇게 C에서 제공하는 변수형 타입을 그냥 사용하는 경우 코드의 재활용성이 떨어지게 된다. 이러한 불확실성을 없애기 위하여 uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t 형으로 변수를 선언하는 것이 좋다. 이렇게 선언하면 어떤 CPU나 컴파일러를 사용하더라도 동일한 크기의 변수를 선언할 수 있기 때문이다.
15번줄에 나오는 표현은 주의 깊게 볼 필요가 있다.
volatile struct port *const portb = (void*)PORTB_REG;
위의 코드에서 'volatile'과 'const' 키워드가 사용되는 것을 볼 수 있다.
volatile 키워드는 한마디로 얘기해서 volatile로 선언된 변수는 컴파일러 멋대로 최적화를 하지 말고 코드에 있는 그대로 어셈블로 변환하라는 컴파일러에게 내리는 지시어다.
"hello, world"에서 설명했듯이 컴파일 과정중에는 프로그램 최적화 과정이 포함되어 있다. 일반 컴퓨터 프로그램에서는 최적화로 인한 문제가 거의 발생하지 않지만, 임베디드 시스템에서는 최적화로 인하여 예상치 못한 동작을 하는 경우가 있다.
예를 들어 어떤 부품은 하나의 레지스터를 통하여 부품안에 있는 내부 레지스터 주소와 그 안에 들어갈 값을 넣어주게 되는 경우가 있다. 예를 들면 10번 레지스터에 0x78을 쓴다고 하면 다음과 같은 코드가 된다.
control_reg = 10;
control_reg = 0x78;
위의 코드를 컴파일러가 볼때는 첫번째로 수행하는 control_reg = 10을 수행할 필요가 없는것으로 판단한다. 어차피 바로 다음에 실행하는 코드가 같은 레지스터에 0x78을 쓰므로 앞의 코드를 굳이 수행할 필요없이 두번째 코드만 실행해도 문제없을거라고 판단하기 때문이다. 그러나 실제로는 반드시 주소먼저 쓰고 값을 써야하기 때문에 코드를 최적화 하면 안되고 코드에 나와 있는 그대로 수행을 하여야 한다. 이렇게 컴파일러에게 멋대로 최적화 하지 말것을 알려주기 위하여 volatile 키워드를 사용한다. 임베디드 시스템의 레지스터에 값을 쓰기 위하여 변수 선언이 필요할 때 습관적으로 volatile을 선언해주는 것이 좋다.
const 키워드는 변수를 선언할 때 상수형으로 만들기 위하여 선언한다. 즉 초기값으로 한번 설정된 이후에는 절대 다른값으로 변경되면 안될 경우에 사용한다.
앞의 코드에서 portb는 PORT B블럭의 시작 주소를 초기값으로 설정되도록 되어 있다. 이 주소는 프로그램이 수행중이더라도 변경되어서는 안되는 값이다. 다시 말해서 PORT B의 주소는 가변적이지 않기 때문이다. 따라서 혹시 코드내에서 portb를 다른 주소값으로 변경하는 부분이 있으면 컴파일 과정에서 에러를 발생시킨다.
실제로 임베디드 시스템 프로그램을 하다보면 포인터 변수에 들어 있는 주소 값이 어느틈엔가 다른 주소로 변경되어 있는 경우가 있다. 그런데 코드안에서 주소를 바꾸는 곳을 찾는게 때로는 쉽지 않을때가 있다. 이럴때 const 형 포인터 변수를 선언하면 이러한 버그를 사전에 방지할 수 있다.
예를 들어 setup() 함수내에 다음과 같은 코드를 작성하여 컴파일 해보자.
void setup(void)
{
portb = (void*)0x29;
portb->ddr = RED_LED;
portb->port= (RED_LED | PUSH_BTN);
}
컴파일 했을때 위의 그림처럼 컴파일 에러가 발생한다.
volatile과 const 키워드를 적재 적소에 사용하여 코드의 불확실성을 최소화 하도록 한다.
'입문' 카테고리의 다른 글
푸쉬 버튼과 LED제어 연동 1 (0) | 2016.03.12 |
---|---|
LED 제어 - 부록 (3) | 2016.03.07 |
푸쉬 버튼 감지 1 (7) | 2016.03.06 |
LED 제어 7 (13) | 2016.03.05 |
LED 제어 6 (1) | 2016.03.05 |