Skip to content

예제 printf_scanf

irmusy edited this page Sep 30, 2013 · 2 revisions

개요

보통 MCU에는 printf()나 scanf() 함수가 없는 경우가 많습니다. 모니터나 키보드가 있는 PC에서는 당연한 함수인 printf()와 scanf()가 없다는 사실은 초보 MCU 개발자들을 당황하게 만들곤 합니다.

본 예제에서는 UART를 이용해 printf()와 scanf() 기능을 구현하는 semi-hosting 기법을 안내하고 다른 곳에서도 쉽게 가져다 사용할 수 있게끔 만들어둔 low-level I/O에 대해 살펴보도록 하겠습니다.

Semi-hosting이라는 것은 모니터와 키보드가 없는 시스템에 다른 대체 수단으로 모니터와 키보드를 만들어 에뮬레이션 해 주는 것이라 생각하시면 됩니다. 대체 수단으로 가장 많이 사용되는 것은 UART입니다. 모니터로 출력할 내용은 UART로 송신하고, UART로부터 수신된 내용은 키보드로 부터 입력받을 내용으로 간주하는 것입니다. UART가 아닌 다른 인터페이스를 사용할 수도 있습니다. Cortex-M3 코어에서는 SWD(Serial Wire Debugger)의 SWO 핀을 이용하여 semi-hosting을 구현할 수도 있습니다.

기본적으로 semi-hosting은 라이브러리에서 제공하는 기능입니다. EWARM 개발 환경에서는 EWARM의 표준 라이브러리인 D-lib에서 관련 기능을 제공합니다. printf()와 scanf() 자체는 EWARM의 D-lib에 구현되어 있습니다. 사용자가 printf()를 호출했을 때 D-lib에 있는 printf()함수가 호출되고, 그 속에서 출력할 메시지를 구성하게 됩니다. 최종적으로 물리적인 인터페이스를 통한 출력의 구현은은 사용자 몫으로 남겨져 있습니다. Semi-hosting을 위한 인터페이스가 어떻게 될 지는 사용자만이 알고 있기 때문입니다. 이처럼 사용자가 구현해 줘야 하는 부분은 low-level I/O 라 부릅니다. 사용자가 할 일은 semi-hosting에서 구현을 요구하는 low-level I/O 함수들의 원형대로 그 내용을 채워 넣어 주는 것입니다.

UART를 이용한 semi-hosting을 위한 low-level I/O는 common/llio.c 파일에서 구현하고 있습니다. EWARM에서는 semi-hosting을 위한 low-level I/O로 **__write()**함수와 **__read()**함수를 필요로 합니다.

  • __write() : printf()가 호출되었을 때 최종적으로 UART 인터페이스로 송신하는 코드
  • __read() : scanf()가 호출되었을 때 UART로 부터 수신하여 버퍼를 반환하는 코드

low-level I/O와 semi-hosting에 관한 보다 자세한 내용은 EWARM 설치 폴더의 arm/doc/EWARM_DevelopmentGuide.ENU.pdf 파일을 참조하시기 바랍니다.

관련 Peripheral

  • SysCtl
  • UART
  • GPIO

소스 구성

  • printf_scanf/printf_scanf.c : 예제 소스 파일
  • common/llio.c : UART를 이용한 low-level I/O 구현 소스
  • common/llio.h : low-level I/O 헤더

소스 살펴보기

##common/llio.c

6 line:

#include <stdio.h>
#include <stddef.h>
#include <yfuns.h>

#include "hw_types.h"
#include "hw_memmap.h"

#include "gpio.h"
#include "uart.h"

필요한 헤더파일들을 include하고 있습니다. <>로 둘러싸인 헤더 파일들은 EWARM에서 제공하는 헤더 파일들입니다. 나머지는 다른 예제와 마찬가지로 StellarisWare에서 제공하는 파일들입니다.

18 line:

void llio_init(unsigned long baudrate)
{
    //
    // Configure UART
    //
    SysCtlPeripheralEnable(SYSCTL_PERIPH_UART0);
    SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOA);
    GPIOPinTypeUART(GPIO_PORTA_BASE, GPIO_PIN_0 | GPIO_PIN_1);
    UARTConfigSetExpClk(UART0_BASE, SysCtlClockGet(), baudrate,
                        (UART_CONFIG_WLEN_8 | UART_CONFIG_STOP_ONE |
                         UART_CONFIG_PAR_NONE));
}

Low-level I/O에서 사용하는 장치들을 초기화 하는 함수입니다. 이 함수는 EWARM에서 자동으로 호출해 주지 않으며, 사용자가 필요한 시점에 main() 함수 내에서 호출해 장치를 초기화 시켜줘야 합니다.

EWARM에서 자동으로 호출하게끔 만들 수도 있으나 LM3S칩셋의 특성상 바람직 하지 않습니다. LM3S MCU는 시스템 클럭을 수시로 바꿀 수 있습니다. 시스템 클럭 속도가 변경되면 UART에서 baudrate 관련 설정도 따라서 바뀌어야 합니다. 만일 EWARM에서 시스템 부팅 시점에 llio_init()함수를 자동으로 한번만 불러준다면 이러한 동적 변화에 자유롭게 대응하기 어렵게 됩니다. 이곳에서 살펴 볼 llio_init()함수는 인자로 baudrate를 전달 받게끔 디자인되어 있으며, 사용자가 원하는 시점에 원하는 속도로 UART 인터페이스를 초기화 할 수 있습니다.

32 line:

#pragma module_name = "?__write"
static int MyLowLevelPutchar(int ch);
static int MyLowLevelGetchar();

Low-level I/O에서 사용 될 함수들을 선언하고 있습니다.

#pragma 문구는 뒤에 따라오는 함수가 일반 목적의 함수가 아닌 low-level I/O를 위한 특수 목적 함수임을 컴파일러와 링커에 알려주기 위한 것입니다.

36 line:

size_t __write(int handle, const unsigned char * buffer, size_t size)
{
  /* Remove the #if #endif pair to enable the implementation */

  size_t nChars = 0;

  if (buffer == 0)
  {
    /*
     * This means that we should flush internal buffers.  Since we
     * don't we just return.  (Remember, "handle" == -1 means that all
     * handles should be flushed.)
     */
    return 0;
  }

  /* This template only writes to "standard out" and "standard err",
   * for all other file handles it returns failure. */
  if (handle != _LLIO_STDOUT && handle != _LLIO_STDERR)
  {
    return _LLIO_ERROR;
  }

  for (/* Empty */; size != 0; --size)
  {
    if (MyLowLevelPutchar(*buffer++) < 0)
    {
      return _LLIO_ERROR;
    }

    ++nChars;
  }

  return nChars;
}

**__write()**함수의 구현입니다. 이 코드는 EWARM에서 제공하는 기본 소스 그대로 입니다. printf()를 통해 출력할 문자열이 만들어 지면 그 문자열 내용과 길이를 인자로 하여 **__write()**함수가 호출됩니다. 이 문자열을 전달받아 한글자씩 **MyLowLevelPutchar()**함수를 호출해 주고 있습니다. 이 **MyLowLevelPutchar()**함수가 사용자가 직접 작성해야 하는 low-level I/O 중 출력 함수입니다.

72 line:

static int MyLowLevelPutchar(int ch)
{

    UARTCharPut(UART0_BASE, ch);

    return 1;
}

Low-level I/O 중 한글자 출력 함수를 구현합니다. DriverLib에 있는 **UARTCharPut()**함수를 그대로 사용하고 있습니다.

81 line:

size_t __read(int handle, unsigned char * buffer, size_t size)
{
  /* Remove the #if #endif pair to enable the implementation */

  int nChars = 0;

  /* This template only reads from "standard in", for all other file
   * handles it returns failure. */
  if (handle != _LLIO_STDIN)
  {
    return _LLIO_ERROR;
  }

  for (/* Empty */; size > 0; --size)
  {
    int c = MyLowLevelGetchar();
    if (c < 0)
      break;

    *buffer++ = c;
    ++nChars;
  }

  return nChars;
}

**__write()**와 마찬가지로 scanf()가 호출되었을 때 불려 질 **__read()**함수를 정의합니다. **__read()**함수는 UART로 부터 수신한 데이터를 버퍼에 담아 반환하는 기능을 합니다. 이 함수 역시 EWARM에서 기본 제공하는 소스 코드 그대로입니다. 사용자가 작성해야 할 부분은 다음에 나오는 MyLowLevelGetchar() 함수입니다.

107 line:

static int MyLowLevelGetchar()
{
    return (int)UARTCharGet(UART0_BASE);
}

Low-level I/O 중 read 부분에 해당하는 함수 구현입니다. UART 장치로 부터 한 글자의 데이터를 읽어 반환하는 간단한 함수입니다.

printf_scanf.c

61 line:

    //
    // Configure low-level I/O to use printf() and scanf()
    //
    llio_init(115200);

Low-level I/O 관련 장치 초기화를 위한 코드입니다. UART0를 사용하고 115200bps로 초기화 합니다. 이후로는 printf()와 scanf()를 사용할 수 있습니다.

68 line:

    printf("\r\n\r\nLow-level I/O Example\r\n");
    printf("Enter any number...\r\n");

일반적인 printf()와 사용법은 동일합니다.

77 line:

        if (scanf("%d", &n) == 1)
        {
            printf("%d is entered.\r\n", n);

            printf("\r\nEnter any number...\r\n");
        }
        else
            scanf("%s", buff);      // flush read FIFO

scanf()역시 일반적인 사용법과 동일합니다. PC에서와 마찬가지로 scanf() 문구를 만나면 이곳에서 실행을 멈추고 적절한 입력이 들어올 때 까지 기다립니다. 임베디드 환경에서 blocking I/O를 사용하는 것은 아주 위험하므로 주의해야 할 부분입니다.

printf()에서 % 문자를 사용하는 방법 역시 동일합니다.

scanf의 리턴 값은 정상 동작했을 경우 scanf()에서 사용한 % 의 개수 만큼 돌아옵니다. 본 예제에서는 %d 하나만 사용했기 때문에 정상이라면 1이 리턴되어야 합니다. 1이 아닌 다른 값이 리턴된 경우는 오류 상황인 것으로 간주해야 하며, 이때에는 입력 버퍼의 내용을 비워서 오류가 무한 반복 되는 것을 막아야 합니다. 83~84 line의 내용을 주석으로 막아두고 실행해 보면서 숫자 대신 다른 문자(방향키나 Function 키 등)을 입력해 보시면 read FIFO를 flush 하는 것의 필요성을 확인하실 수 있습니다.

실행 방법 안내

  1. 펌웨어 다운로드 합니다.

  2. PC에서 ComPortMaster를 실행하고 Stellaris-JTAG의 가상 시리얼 포트를 open 합니다. 이때 설정은 115200bps로 합니다.

  3. 보드상의 리셋 버튼을 1회 누릅니다. ComPortMaster의 수신 창에 아래와 같은 메시지가 출력되는 것을 확인합니다.

     Low-level I/O Example
     Enter any number...
    
  4. ComPortMaster에서 임의의 숫자를 입력하여 CR, LF와 함께 send하면 입력한 숫자와 함께 메시지가 출력되는 것을 확인할 수 있습니다.