AVR

AVR에서 printf 함수 사용하기

a1de61c172 2016. 10. 8. 23:59

서론

 여러분은 C언어를 처음 배울 때, printf("Hello, World!\n");로 시작했던 것을 기억하실 텐데요. AVR을 하면서 제일 당혹스러웠던 점은 가장 간단하고, 기본이라 생각되었던 printf함수를 기본적으로는 사용하지 못 한다는 사실이었습니다. AVR에는 OS가 없기 때문에 프로그램은 표준 IO를 사용하기 위해 추가적인 정보를 제공해야 합니다. 바로 표준 스트림을 정의해야 하는데요. 표준 스트림이란 입력을 위한 stdin, 출력을 위한 stdout, 오류 출력을 위한 stderr입니다. 우리가 지금껏 C언어를 해오면서 이러한 스트림을 생성하지 않았던 이유는 프로그램이 실행되면 OS가 자동으로 처리해주었기 때문입니다.[각주:1] 하지만 앞서 말씀드렸다시피 AVR에는 OS가 없기 때문에 개발자가 직접 이러한 작업을 해주어야 합니다.
 저 같은 경우에는 printf 함수를 디버깅하기 위해 사용합니다. JTAG를 사용해서 MCU 내부 상태를 확인할 수 있으면 가장 좋겠지만 JTAG 장비가 없기도 하고, 간단히 시리얼로 확인하고 싶은 것도 하나의 이유입니다.

표준 스트림 초기화[각주:2]

 표준 스트림 atdin, stdout, stderr을 초기화 하려면 stdout.h에 들어있는 fdevopen()을 사용하여야 합니다. fdevopen() 함수는 프로그램으로부터 실제 장치의 스트림을 열어 줍니다. 만약 성공하게 되면 열린 스트림에 대한 포인터를 반환하는데, 가장 처음 열린 입력 스트림은 stdin, 출력 스트림은 각각 stdout, stderr에 할당됩니다.
fdevopen()함수의 프로토타입은 다음과 같습니다.

FILE* fdevopen( int(*)(char, FILE*) put, int(*)(FILE *) get)

 첫 번째 인자는 출력 스트림을 할당하는 함수 포인터 입니다. 두 번째 인자는 입력 스트림을 할당하는 함수 포인터 입니다. 꼭 입력 스트림까지 모두 할당할 필요는 없이 출력 스트림만 할당해도 무방합니다. 아래 예제는 USART0를 stdout로 초기화 하는 예제로 USART를 설정하는 부분과 관련된 함수는 제외 되었습니다.

소스코드

#include <avr/io.h>
#include <stdio.h>
#include <stdbool.h>

static int usartTxChar(char, FILE*);

int main(void) {
/*
* 표준 출력 스트림 초기화
*/
FILE* fpStdio = fdevopen(usartTxChar, NULL);

usartInit();

printf("Hello, World!\r\n");

while(true);
}

int usartTxChar(char ch, FILE *fp) {
while (!(UCSR0A & (1 << UDRE)));

UDR0 = ch;

return 0;
}

실행결과

Floating point 문제

 위 예제를 통해 stdout를 초기화 했습니다. 이제 printf함수를 사용할 수 있는데 float, double은 출력할 수 없습니다. 만약 출력을 시도한다면 아래와 같이 나올 것입니다.

소스코드

#include <avr/io.h>
#include <stdio.h>
#include <stdbool.h>

static int usartTxChar(char, FILE*);

int main(void) {
/*
* 표준 출력 스트림 초기화
*/
FILE* fpStdio = fdevopen(usartTxChar, NULL);

usartInit();

printf("Hello, Double! %lf\r\n", 10.205);

while(true);
}

int usartTxChar(char ch, FILE *fp) {
while (!(UCSR0A & (1 << UDRE)));

UDR0 = ch;

return 0;
}

실행결과

printf 선택지

 avr-libc에서는 AVR 프로그램 메모리의 크기가 작기 때문에 3개의 선택지를 두었습니다. 기본 값은 float, double과 같은 Floating point에 대한 지원이 없습니다. 즉, 소숫점이 들어가는 자료형은 기본적으로 출력할 수 없습니다. 이는 컴파일러 옵션에서 변경할 수 있습니다. Atmel Studio 5.x이상 버전 부터는 컴파일러 옵션을 오른쪽에 'Solution Explorer >${솔루션 이름} 오른쪽 클릭 > Properties'에서 변경할 수 있습니다.

 위처럼 설정하게 되면 아래와 같이 float, double에 대한 출력이 가능해 집니다. 여기서 최소 버전의 printf를 사용하고 싶으면 'Other Linker Flags'에 '-lprintf_min'을 입력하시면 됩니다.

 다만 이렇게 되면 프로그램 크기가 커지게 됩니다. 같은 코드를 세 가지 각기 다른 printf를 포함 시켰을 때, 프로그램 크기를 비교해보도록 하겠습니다. 아래는 비교할 소스코드입니다. Floating point 지원이 빠진 버전이라도 프로그램 메모리의 크기를 비교하는 것이기에 컴파일에는 지장이 없으므로 그대로 진행했습니다.

소스코드

#include <avr/io.h>
#include <stdio.h>
#include <stdbool.h>

static int usartTxChar(char, FILE*);

int main(void) {
/*
* 표준 출력 스트림 초기화
*/
FILE* fpStdio = fdevopen(usartTxChar, NULL);

usartInit();

printf("Hello, Double! %lf\r\n", 10.205);

while(true);
}

int usartTxChar(char ch, FILE *fp) {
while (!(UCSR0A & (1 << UDRE)));

UDR0 = ch;

return 0;
}

실행결과

 먼저 기본 값입니다.

 기본 printf는 프로그램 메모리의 1.9%를 사용합니다. 그 다음은 Floating point 지원이 포함된 풀 버전입니다.

 프로그램 메모리의 3.1%를 사용하는 것으로 상당히 차이가 많이 나는 것을 확인할 수 있습니다. 마지막으로 기본적인 Integer과 String 지원만 포함된 최소 버전입니다.

 기본 값에 비해서 0.3% 더 적은 프로그램 메모리를 사용합니다. 만약 다른 프로그램 코드가 비대해서 정말 프로그램 메모리의 최적화가 필요하면서, Integer과 String만 필요하다면 최소 버전을 사용하는 게 바람직해 보입니다.

malloc()을 사용하지 않는 표준 스트림

 fdevopen()함수의 문제점이라고 하면 malloc()을 필요로 합니다. 이는 제한된 자원의 MCU에서는 문제가 될 소지가 있습니다. 이를 위해 avr-libc는 fdev_setup_stream()을 제공하여 malloc()가 전혀 사용되지 않고 fdevopen()과 같은 동작을 할 수 있습니다. 아래 코드는 fdev_setup_stream()을 사용하여 표준 스트림을 할당하는 예제입니다.

소스코드

#include <avr/io.h>
#include <stdio.h>
#include <stdbool.h>

static int usartTxChar(char, FILE*);

static FILE myStdout = FDEV_SETUP_STREAM(usartTxChar, NULL, _FDEV_SETUP_WRITE);

int main(void) {
serialInit();
stdout = &myStdout;

printf("Hello, World!\r\n");

while (true);
}

int usartTxChar(char ch, FILE *fp) {
while (!(UCSR0A & (1 << UDRE)));

UDR0 = ch;

return 0;
}

실행결과

 예제에서는 FDEV_SETUP_STREAM() 매크로를 사용해 모든 초기화가 컴파일 타임에 되도록 했습니다. 실행 결과는 맨 처음 printf를 사용했을 때와 별반 차이가 없습니다. 다만 malloc()를 사용하지 않았기 때문에 프로그램 메모리를 절약할 수 있습니다. 만약 스트림이 더 이상 필요 없다면 이 경우에는 fdev_close()매크로를 호출하여야 합니다. 만약 fclose()를 사용하면 된다면 그 자체로는 문제가 되지 않지만 링커가 malloc() 모듈을 링크할 수 있습니다.

Flash ROM 문자 출력

 printf는 SRAM에 있는 문자만 출력 할 수 있는 것은 아닙니다. 만약 변하지 않는 고정된 문자열의 경우에는 SRAM의 사용량을 줄이기 위해 Flash ROM에 문자열을 넣어두고 그 문자열을 출력할 수도 있습니다. printf_P() 함수와 PSTR() 매크로가 그 기능을 지원합니다. PSTR 매크로를 사용하려면 'avr/pgmspace.h'가 필요합니다. 아래 코드는 Flash ROM에 있는 문자열을 출력하는 예제입니다.

소스코드

#include <avr/io.h>
#include <avr/pgmspace.h>
#include <stdio.h>
#include <stdbool.h>

static int usartTxChar(char, FILE*);

int main(void) {
serialInit();

fdevopen(usartTxChar, NULL);

printf_P(PSTR("Hello, World!\r\n"));

while (true);
}

int usartTxChar(char ch, FILE *fp) {
while (!(UCSR0A & (1 << UDRE)));

UDR0 = ch;

return 0;
}

실행결과

 실행 결과를 보면 제일 처음 우리가 printf를 사용했을 때와 아무런 차이가 안나는 것 처럼 보일 수도 있습니다. 하지만 데이터 메모리 사용량을 보면 큰 차이가 있다는 것을 확인할 수 있습니다. 첫 번째로 printf()를 사용해서 "Hello, World!\r\n"을 출력할 때의 메모리 사용량입니다.

 그 다음은 printf_P(PSTR("Hello, World!\r\n"))을 사용해 Flash ROM에 있는 문자열을 출력할 때의 메모리 사용량입니다.

 차이가 나는게 보이시나요? 이렇듯 고정된 문자열을 출력할 때는 메모리 사용량을 줄이기 위해서라도 printf_P()함수와 PSTR() 매크로를 적극 사용해야 할 것입니다.

결론

 C에서 우리가 흔히 사용하던 printf함수를 사용하기 위해 AVR에서는 약간 복잡한 과정이 필요하긴 하지만 포메팅 옵션들 (%d, %f와 같은 옵션)을 사용해 출력을 편하게 할 수 있다는 점에서 디버깅을 할 때 유용하게 쓸 수 있지 않을까 싶습니다. 저 같은 경우에는 약간의 프로그램 메모리를 희생하더라도 간단함을 추구하기 때문에 printf함수를 적극 이용하고 있습니다. 이상으로 AVR에서 printf 함수를 사용하기 위한 강좌를 마치도록 하겠습니다.

  1. Standard streams, https://en.wikipedia.org/wiki/Standard_streams [본문으로]
  2. Avr-libc , http://www.nongnu.org/avr-libc/user-manual/group__avr__stdio.html [본문으로]