리눅스 시리얼 통신 프로그래밍 가이드

By | 2026년 7월 9일
Table of Contents

리눅스 시리얼 통신 프로그래밍 가이드

출처: The Linux Serial-Programming-HOWTO (C) 1997 by Peter Baumann

리눅스 시리얼 통신

포트 세팅

장치 파일인 /dev/ttyS*는 리눅스에서 터미널을 연결하기 위한 목적으로 사용된다. 이 사실은 시리얼 통신 프로그래밍을 하는 데 반드시 기억해야 할 사항이다. 예를 들어, 시리얼 포트도 문자 에코를 하도록 설정되어 있으며, 이 설정은 보통 데이터 전송 시에 바꿔야 할 사항이다.

모든 파라미터들은 프로그램 코드에서 쉽게 설정할 수 있다. 파라미터들은 <asm/termbits.h>에 정의되어 있는 struct termios 구조체에 저장되어 있다.

#define NCCS 19

struct termios {
    tcflag_t c_iflag;    /* input mode flags */
    tcflag_t c_oflag;    /* output mode flags */
    tcflag_t c_cflag;    /* control mode flags */
    tcflag_t c_lflag;    /* local mode flags */
    cc_t     c_line;     /* line discipline */
    cc_t     c_cc[NCCS]; /* control characters */
};

각 플래그 설명

플래그 설명
c_iflag 입력 처리(input processing) 방식 정의. read()로 읽기 전에 수신 데이터를 처리하는 방법 설정
c_oflag 출력 처리(output processing) 방식 정의
c_cflag baudrate, data bits, stop bits 등의 포트 설정
c_lflag echo 여부 등 로컬 동작 결정
c_cc[] EOF, STOP 등 제어 동작에 사용할 문자 정의

c_line 항목은 POSIX 호환 시스템에서 사용되지 않는다.
각 플래그의 상세 설명은 termios(3) man page를 참조하라.

Control Modes(c_cflags)

  • PARENB (Parity)
    PARENB가 세팅되면 parity bit이 enable된다. 대부분의 시리얼 통신은 parity bit을 사용하지 않으므로 확실하지 않은 경우에는 이 bit를 clear하도록 한다.
tty.c_cflag &= ~PARENB; // Clear parity bit, disabling parity (most common)
tty.c_cflag |= PARENB;  // Set parity bit, enabling parity
  • CSTOPB (Num.Stop Bit)
    CSTOPB가 세팅되면 2개의 stop bit가 사용된다. 이 bit가 clear되면 하나의 stop bit가 사용된다. 대부분의 시리얼 통신에서는 하나의 stop bit를 사용한다.
tty.c_cflag &= ~CSTOPB; // Clear stop field, only one stop bit used in communication (most common)
tty.c_cflag |= CSTOPB;  // Set stop field, two stop bits used in communication
  • Number of Bits Per Byte
    CS field에는 몇개의 data bit가 한 바이트 당 전송될것 인지를 설정한다. 대부분의 세팅은 8(CS8)이며 바이트 당 몇개의 data-bit를 사용할 것인지 설정하기 전에 &= ~CSIZE로 clear해줘야 한다.
tty.c_cflag &= ~CSIZE; // Clear all the size bits, then use one of the statements below
tty.c_cflag |= CS5; // 5 bits per byte
tty.c_cflag |= CS6; // 6 bits per byte
tty.c_cflag |= CS7; // 7 bits per byte
tty.c_cflag |= CS8; // 8 bits per byte (most common)
  • CRTSCTS (Flow Control)
    CRTSCTS field가 세팅되면 하드웨어 RTS/CTS가 enable된다. 이 field가 disable되야 하는 상황에서 enable되면 sender쪽에서는 상대가 ready 상태가 되기를 기다리며 무한정 buffer동작을 수행하므로 시리얼 포트에서 데이터 수신이 불가능하다.
ty.c_cflag &= ~CRTSCTS; // Disable RTS/CTS hardware flow control (most common)
tty.c_cflag |= CRTSCTS;  // Enable RTS/CTS hardware flow control
  • CREAD & CLOCAL
    CLOCAL을 세팅하면 carrier detect와 같은 modem-specific한 시그널을 disable하게 되며 controlling process가 modem이 연결 해제 되는것을 감지할 때 SIGHUP 신호를 받을 수 없게 한다. CREAD를 세팅하면 데이터를 read할 수 있게 된다.
tty.c_cflag |= CREAD | CLOCAL; // Turn on READ & ignore ctrl lines (CLOCAL = 1)

Local Modes(c_lflag)

  • Disabling Canonical Mode
    UNIX 시스템은 canonical, non-canonical 두개의 basic input mode를 제공한다. Canononical mode에서는 새로운 line character가 수신될 때마다 input을 처리하고 receiving application에서는 이를 line-by-line으로 처리한다. 이러한 방식은 시리얼 통신에서는 바람직하지 않으므로 보통 canonical mode를 disable한다. 또한 canonical mode에서는 backspace와 같은 것들은 현재의 텍스트 line을 수정하는데 쓰이므로 serial data를 처리할 때 backspace로 인해 특정 byte들이 사라지는 경우를 막기 위해 canonical mode를 disable 한다.
tty.c_lflag &= ~ICANON; //canonical mode is disabled.
  • Echo
    Echo-bit 이 세팅되면 보낸 character들이 다시 되돌아온다.(echoed back)
tty.c_lflag &= ~ECHO; // Disable echo
tty.c_lflag &= ~ECHOE; // Disable erasure
tty.c_lflag &= ~ECHONL; // Disable new-line echo
  • Disable Signal Chars
    ISIG bit가 세팅되면 INTR, QUIT, SUSP가 interpret되며 이러한 상황은 serial port에서 바람직하지 않으므로 ISIG bit를 clear한다.
tty.c_lflag &= ~ISIG; // Disable interpretation of INTR, QUIT and SUSP

Input Modes(c_iflag)

  • Software Flow Control ( IXOFF, IXON, IXANY)
    IXOFF, IXON, IXANY를 clear하면 software flow control을 disable한다. (Serial 통신에서 disable필요)
tty.c_iflag &= ~(IXON | IXOFF | IXANY); // Turn off s/w flow ctrl
  • Disabling Special Handling of Bytes on Receive
    raw data만을 처리하기 위해 아래의 bit들을 모두 clear하여 special handling byte들을 모두 disable한다.
tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); // Disable any special handling of received bytes

Output Modes(c_oflag)

termios struct멤버인 c_oflag는 output처리에 대한 low-level 세팅에 관한 정보를 담고 있다. Serial 통신에서는 모든 output chars/bytes에 대한 special handling을 disable 해야 하므로 아래의 bit들을 모두 clear한다.

tty.c_oflag &= ~OPOST; // Prevent special interpretation of output bytes (e.g. newline chars)
tty.c_oflag &= ~ONLCR; // Prevent conversion of newline to carriage return/line feed
// tty.c_oflag &= ~OXTABS; // Prevent conversion of tabs to spaces (NOT PRESENT IN LINUX)
// tty.c_oflag &= ~ONOEOT; // Prevent removal of C-d chars (0x004) in output (NOT PRESENT IN LINUX)

OXTABS와 ONOEOT는 Linux에서는 정의되어 있지 않지만 Linux에서는 XTABS field를 통해 관련된 일을 처리할 수 있다.


시리얼 장치의 입력 방법

한 문자씩 읽는 루프로 전체 문자열을 받는 방법은 문자 손실이 발생할 수 있으므로 가능하면 피하고, 전체 문자열을 한 번에 읽는 방식을 권장한다.

Canonical 입력 처리 (Canonical Input Processing)

터미널의 기본 처리 방법으로, 한 줄 단위로 데이터를 처리한다. 한 줄의 종료는 다음 문자로 판단한다.

  • NL (New Line, ASCII LF)
  • EOF (End of File)
  • EOL (End of Line)

CR(Carriage Return) 문자는 디폴트 설정에서 줄 종료로 인식되지 않는다.

이 모드에서는 ERASE, DELETE WORD, REPRINT 문자 처리와 CR → NL 변환도 가능하다.


Non-Canonical 입력 처리 (Non-Canonical Input Processing)

한 번 읽을 때마다 정해진 크기의 문자만 읽어낸다. 타이머를 설정하여 일정 시간 내에 read()가 리턴하지 않으면 강제 리턴할 수 있다.

설정 파라미터는 c_cc[VTIME]c_cc[VMIN] 두 가지이다.

조건 동작
MIN > 0, TIME = 0 MIN개의 문자가 올 때까지 무한 대기 후 리턴
MIN = 0, TIME > 0 TIME × 0.1초 타임아웃. 그 전에 문자가 오면 즉시 리턴
MIN > 0, TIME > 0 MIN개 이상의 문자가 오거나 문자 간 간격이 TIME 초과 시 리턴
MIN = 0, TIME = 0 즉시 리턴. 현재 읽을 수 있는 문자 수만큼 반환

비동기 입력

위 두 모드는 동기/비동기 방식 모두 사용 가능하다.

  • 동기 방식 (기본값): read()의 조건이 만족될 때까지 block
  • 비동기 방식: read()가 즉시 리턴되며, 데이터 수신 시 signal handler로 시그널 전달

입력 장치 멀티플렉싱

여러 장치(예: TCP/IP 소켓 + 시리얼 포트)에서 동시에 입력을 받아야 할 때 사용한다.

select() 시스템 호출을 사용하면 CPU 부하 없이 여러 입력을 대기할 수 있다.

루프를 돌면서 입력 여부를 체크하는 polling 방식은 CPU 부하를 유발하여 다른 프로세스의 성능을 저하시킨다.


프로그램 예제

모든 예제는 miniterm.c에서 발췌하였다.
Canonical 모드에서 처리 가능한 최대 문자 수는 255자(<linux/limits.h> 참조)이다.

시리얼 포트 장치 파일과 파일 접근 권한을 반드시 확인하라.

chmod a+rw /dev/ttyS1

예제 1: Canonical 입력 처리

CRTSCTS : 하드웨어 흐름 제어.
시리얼 케이블이 모든 핀에 연결되어 있는 경우만 사용하도록 한다.
(Serial-HOWTO의 7장을 참조)

핀이 연결되어 있지 않은 상태에서 활성화하면 오작동한다.

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <stdio.h>

/* Baudrate 설정은 <asm/termbits.h>에 정의 (<termios.h>에서 include됨) */
#define BAUDRATE B38400
/* COM1="/dev/ttyS1", COM2="/dev/ttyS2" */
#define MODEMDEVICE "/dev/ttyS1"
#define _POSIX_SOURCE 1  /* POSIX 호환 소스 */
#define FALSE 0
#define TRUE  1

volatile int STOP = FALSE;

int main()
{
    int fd, c, res;
    struct termios oldtio, newtio;
    char buf[255];

    /*
     * 읽기/쓰기 모드로 장치를 연다 (O_RDWR)
     * O_NOCTTY: CTRL-C 등이 프로그램을 종료하지 않도록 controlling tty 방지
     */
    fd = open(MODEMDEVICE, O_RDWR | O_NOCTTY);
    if (fd < 0) { perror(MODEMDEVICE); exit(-1); }

    tcgetattr(fd, &oldtio);          /* 현재 포트 설정 저장 */
    bzero(&newtio, sizeof(newtio));  /* 새 설정 구조체 초기화 */

    /*
     * BAUDRATE : 전송 속도 (cfsetispeed/cfsetospeed로도 설정 가능)
     * CRTSCTS  : 하드웨어 흐름 제어 (모든 핀 연결 시에만 사용)
     * CS8      : 8N1 (8bit, no parity, 1 stopbit)
     * CLOCAL   : 로컬 연결 (모뎀 제어 안 함)
     * CREAD    : 문자 수신 활성화
     */
    newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD;

    /*
     * IGNPAR : parity 에러 문자 무시
     * ICRNL  : CR 문자를 NL 문자로 변환
     */
    newtio.c_iflag = IGNPAR | ICRNL;

    newtio.c_oflag = 0;         /* Raw output */
    newtio.c_lflag = ICANON;    /* Canonical 입력 활성화, echo 비활성화 */

    /* 제어 문자 초기화 */
    newtio.c_cc[VINTR]    = 0;  /* Ctrl-c */
    newtio.c_cc[VQUIT]    = 0;  /* Ctrl-\ */
    newtio.c_cc[VERASE]   = 0;  /* del */
    newtio.c_cc[VKILL]    = 0;  /* @ */
    newtio.c_cc[VEOF]     = 4;  /* Ctrl-d */
    newtio.c_cc[VTIME]    = 0;  /* inter-character timer 미사용 */
    newtio.c_cc[VMIN]     = 1;  /* 1문자 올 때까지 blocking */
    newtio.c_cc[VSWTC]    = 0;  /* '\0' */
    newtio.c_cc[VSTART]   = 0;  /* Ctrl-q */
    newtio.c_cc[VSTOP]    = 0;  /* Ctrl-s */
    newtio.c_cc[VSUSP]    = 0;  /* Ctrl-z */
    newtio.c_cc[VEOL]     = 0;  /* '\0' */
    newtio.c_cc[VREPRINT] = 0;  /* Ctrl-r */
    newtio.c_cc[VDISCARD] = 0;  /* Ctrl-u */
    newtio.c_cc[VWERASE]  = 0;  /* Ctrl-w */
    newtio.c_cc[VLNEXT]   = 0;  /* Ctrl-v */
    newtio.c_cc[VEOL2]    = 0;  /* '\0' */

    tcflush(fd, TCIFLUSH);
    tcsetattr(fd, TCSANOW, &newtio);

    /* 첫 문자가 'z'이면 종료 */
    while (STOP == FALSE) {
        res = read(fd, buf, 255);
        buf[res] = 0;  /* null 종료 문자 추가 */
        printf(":%s:%d\n", buf, res);
        if (buf[0] == 'z') STOP = TRUE;
    }

    tcsetattr(fd, TCSANOW, &oldtio);  /* 이전 포트 설정 복원 */
    return 0;
}

Non-Canonical 입력 처리

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <stdio.h>

#define BAUDRATE    B38400
#define MODEMDEVICE "/dev/ttyS1"
#define _POSIX_SOURCE 1
#define FALSE 0
#define TRUE  1

volatile int STOP = FALSE;

main()
{
    int fd, c, res;
    struct termios oldtio, newtio;
    char buf[255];

    fd = open(MODEMDEVICE, O_RDWR | O_NOCTTY);
    if (fd < 0) { perror(MODEMDEVICE); exit(-1); }

    tcgetattr(fd, &oldtio);

    bzero(&newtio, sizeof(newtio));
    newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD;
    newtio.c_iflag = IGNPAR;
    newtio.c_oflag = 0;
    newtio.c_lflag = 0;  /* Non-canonical, echo 없음 */

    newtio.c_cc[VTIME] = 0;  /* inter-character 타이머 비활성화 */
    newtio.c_cc[VMIN]  = 5;  /* 최소 5문자 수신 전까지 blocking */

    tcflush(fd, TCIFLUSH);
    tcsetattr(fd, TCSANOW, &newtio);

    while (STOP == FALSE) {
        res = read(fd, buf, 255);  /* 최소 5문자 수신 시 리턴 */
        buf[res] = 0;
        printf(":%s:%d\n", buf, res);
        if (buf[0] == 'z') STOP = TRUE;
    }

    tcsetattr(fd, TCSANOW, &oldtio);
}

비동기 입력

#include <termios.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/signal.h>
#include <sys/types.h>

#define BAUDRATE    B38400
#define MODEMDEVICE "/dev/ttyS1"
#define _POSIX_SOURCE 1
#define FALSE 0
#define TRUE  1

volatile int STOP = FALSE;

void signal_handler_IO(int status);  /* signal handler 선언 */
int wait_flag = TRUE;                /* 시그널 수신 전까지 TRUE */

main()
{
    int fd, c, res;
    struct termios oldtio, newtio;
    struct sigaction saio;
    char buf[255];

    /* Non-blocking 모드로 시리얼 장치 오픈 */
    fd = open(MODEMDEVICE, O_RDWR | O_NOCTTY | O_NONBLOCK);
    if (fd < 0) { perror(MODEMDEVICE); exit(-1); }

    /* 비동기 모드 전환 전에 signal handler 설치 */
    saio.sa_handler  = signal_handler_IO;
    saio.sa_mask     = 0;
    saio.sa_flags    = 0;
    saio.sa_restorer = NULL;
    sigaction(SIGIO, &saio, NULL);

    fcntl(fd, F_SETOWN, getpid());   /* SIGIO 수신 등록 */
    fcntl(fd, F_SETFL, FASYNC);      /* 비동기 모드 활성화 */

    tcgetattr(fd, &oldtio);
    newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD;
    newtio.c_iflag = IGNPAR | ICRNL;
    newtio.c_oflag = 0;
    newtio.c_lflag = ICANON;
    newtio.c_cc[VMIN]  = 1;
    newtio.c_cc[VTIME] = 0;
    tcflush(fd, TCIFLUSH);
    tcsetattr(fd, TCSANOW, &newtio);

    while (STOP == FALSE) {
        printf(".\n"); usleep(100000);
        /* SIGIO 수신 후 wait_flag가 FALSE가 되면 데이터 읽기 */
        if (wait_flag == FALSE) {
            res = read(fd, buf, 255);
            buf[res] = 0;
            printf(":%s:%d\n", buf, res);
            if (res == 1) STOP = TRUE;  /* CR만 입력되면 종료 */
            wait_flag = TRUE;
        }
    }

    tcsetattr(fd, TCSANOW, &oldtio);
}

/*
 * signal handler: wait_flag를 FALSE로 설정하여
 * 메인 루프에 데이터 수신을 알린다.
 */
void signal_handler_IO(int status)
{
    printf("received SIGIO signal.\n");
    wait_flag = FALSE;
}

입력 장치 멀티플렉싱

select()를 사용하면 여러 파일 디스크립터를 동시에 감시할 수 있다. 이 방법은 시리얼 포트뿐만 아니라 파일 디스크립터를 사용하는 모든 입출력에 적용 가능하다.

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

main()
{
    int    fd1, fd2;
    fd_set readfs;
    int    maxfd;
    int    loop = 1;

    fd1 = open_input_source("/dev/ttyS1");  /* COM2 */
    if (fd1 < 0) exit(0);
    fd2 = open_input_source("/dev/ttyS2");  /* COM3 */
    if (fd2 < 0) exit(0);

    maxfd = MAX(fd1, fd2) + 1;

    while (loop) {
        FD_SET(fd1, &readfs);
        FD_SET(fd2, &readfs);
        select(maxfd, &readfs, NULL, NULL, NULL);  /* 입력 대기 */

        if (FD_ISSET(fd1, &readfs))
            handle_input_from_source1();
        if (FD_ISSET(fd2, &readfs))
            handle_input_from_source2();
    }
}

타임아웃 설정 방법

int res;
struct timeval Timeout;

/* select() 호출마다 값을 재설정해야 한다 (select가 값을 감소시킴) */
Timeout.tv_usec = 0;  /* 마이크로초 */
Timeout.tv_sec  = 1;  /* 초 */

res = select(maxfd, &readfs, NULL, NULL, &Timeout);
if (res == 0) {
    /* 타임아웃 발생 (입력 없이 1초 경과) */
}

Timeout 값은 select() 호출 시 내부적으로 감소하므로, 루프 안에서 다시 호출할 때마다 반드시 재설정해야 한다.


정리

입력 방식 특징 적합한 상황
Canonical 줄 단위 처리, 기본 터미널 동작 줄 단위 통신 상대방과의 통신
Non-Canonical 고정 크기 읽기, 타이머 지원 고정 패킷 크기 통신, 대량 전송
비동기 시그널 기반, 즉시 리턴 대기 중 다른 작업이 필요한 경우
멀티플렉싱 select() 기반 다중 입력 감시 여러 장치 동시 처리

답글 남기기