리눅스 시리얼 통신 프로그래밍 가이드
출처: 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
CSfield에는 몇개의 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() 기반 다중 입력 감시 |
여러 장치 동시 처리 |