Serial Port Programming under Linux using C/C++
UNIX계열 OS에서는 시리얼 포트를 파일로 표현한다. 일반적으로 /dev/아래에 tty로 시작하는 이름으로 표시되어 있다. 시리얼 포트에 wrtie를 하면 파일을 write하는 것이고 시리얼 포트에서 read 하는것은 파일로부터 read하는 것과 같다.
표시되는 파일들의 예로는 다음과 같은 것들이 있다.
/dev/ttyACM0 : USB에의 Abstract Control Model modem을 의미. Arduino UNO와 같은 것들이 여기에 표시됨.
/dev/ttyPS0 : Yocto 기반의 Linux를 이용하는 Xilinx Zynq FPGA의 default serial port.(Getty connection)
/dev/ttyS0 : Standard COM port. 최근의 데스크탑, 노트북은 COM ports가 별로없어서 잘 사용되지 않음.
/dev/ttyUSB0 : USB-to Serial 케이블이 대부분 여기에 표시됨.
/dev/pts/0 : pseudo terminal. socket으로 생성 가능.
참고) Getty : terminal line을 관리하는 프로그램. 허가받지 않은 접근으로부터 시스템을 보호한다. systemd에 의해 getty 프로세스가 실행되며 하나의 terminal line을 관리한다.
사용하는 헤더
// C library headers
#include <stdio.h>
#include <string.h>
// Linux headers
#include <fcntl.h> // Contains file controls like O_RDWR
#include <errno.h> // Error integer and strerror() function
#include <termios.h> // Contains POSIX terminal control definitions
#include <unistd.h> // write(), read(), close()
/dev/ 아래에 표시되는 시리얼 포트 장치를 open()함수를 통해 열고 open()함수의 return값인 file descriptor를 저장한다.
int serial_port = open("/dev/ttyUSB0", O_RDWR);
// Check for errors
if (serial_port < 0) {
printf("Error %i from open: %s\n", errno, strerror(errno));
}
가장 많이 발생하는 에러는 errno = 2 와 strerror(errno)( No such file or directory를 return). 이러한 에러가 발생하면 경로를 다시 확인하거나 실제로 그 장치가 연결되어 있는지 확인한다.
errno = 13은 permission denied 에러로 현재 유저가 dialout group에 속하지 않을 때 발생한다. 이 때는
$sudo adduser $USER dialout
으로 현재 유저를 dialout group에 추가해준다. 이 때 로그아웃 한 뒤 다시 해야 적용된걸 확인할 수 있다.
위 방법으로도 해결되지 않으면
$sudo chmod 666 /dev/ttyUSB0
로 포트의 권한을 모든 사용자가 쓸 수 있게 한다.
termios struct
#include <termios.h>
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 */
};
termios general terminal은 비동기(asynchronous) 통신 장비에 대해 interface를 제공한다. NuTCRACKER 플랫폼이 시리얼 통신 포트를 지원하며 콘솔 윈도우에도 인터페이스를 지원한다.
시리얼 포트를 사용하기 위해서 설정해줘야 할 것들이 있는데 이를 위해 termios에 접근해야 한다. 새로운 termios struct를 만들고 tcgetattr()함수를 이용해 시리얼 포트의 configuration들을 write한다. tcsetattr()함수를 이용해 세팅한 것들을 저장할 수 있다.
configuration value를 수정할 때에는 오직 관심있는 bit만 수정하고 다른 bit들은 수정하지 않아야 한다. 따라서 = 연산이 아닌 &=나 |= 연산자를 사용해야 한다.
Control Modes(c_cflags)
termios struct의 c_cflag는 control parameter fields에 대한 정보를 담고있다.
1. 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
2. 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
3. Number of Bits Per Byte
CS<number> 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)
4. 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
5. 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)
1. 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.
2. 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
3. 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)
c_iflag는 termios struct의 멤버이며 input 처리에 대해 low-level 세팅에 관한 정보를 담고 있다. c_iflag 멤버의 자료형은 int이다.
1. 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
2. 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를 통해 관련된 일을 처리할 수 있다.
VMIN & VTIME (c_cc)
VMIN이 0으로 세팅되면 VTIME은 read() call이 시작될 때 부터의 time-out을 나타내며 VMIN이 0보다 크게 세팅되면 VTIME은 처음 character를 receive할 부터의 time-out을 나타낸다.
VMIN = 0, VTIME = 0: No blocking, return immediately with what is available
VMIN > 0, VTIME = 0: This will make read() always wait for bytes (exactly how many is determined by VMIN), so read() could block indefinitely.
VMIN = 0, VTIME > 0: This is a blocking read of any number of chars with a maximum timeout (given by VTIME). read() will block until either any amount of data is available, or the timeout occurs. This happens to be my favourite mode (and the one I use the most).
VMIN > 0, VTIME > 0: Block until either VMIN characters have been received, or VTIME after first character has elapsed. Note that the timeout for VTIME does not begin until the first character is received.
VMIN, VTIME은 모두 cc_t 타입으로 정의되어 있으며 이는 unsigned char(1 byte)의 alias이므로 VMIN character의 상한이 255이다. 따라서 최대 timeout은 25.5 seconds(255 deciseconds)이다.
OS의 latency, serial port speed, hardware buffer등에 따라 데이터를 받자마자 return해도 한 바이트씩 받을 수 없다. 만약 1초를 기다려서 데이터를 받자마자 return 하기 위해서는 다음과 같이 한다.
tty.c_cc[VTIME] = 10; // Wait for up to 1s (10 deciseconds), returning as soon as any data is received.
tty.c_cc[VMIN] = 0;
Baud Rate
Bit fields를 사용하는 다른 세팅과 달리 serial port의 baud rate은 cfsetispeed(), cfsetospeed()함수를 호출함으로써 세팅된다. 이때 사용하고자 하는 tty struct의 포인터와 enum을 passing한다.
// Set in/out baud rate to be 9600
cfsetispeed(&tty, B9600);
cfsetospeed(&tty, B9600);
UNIX compliat를 준수하기 위해서는 다음의 baud rate중에서 선택한다.
B0, B50, B75, B110, B134, B150, B200, B300, B600, B1200, B1800, B2400, B4800, B9600, B19200, B38400, B57600, B115200, B230400, B460800
cfsetspeed()를 통해 input과 output의 속도를 동일하게 세팅할 수 있다.
cfsetspeed(&tty, B9600); //Set both the input and output speeds at the same time.
1. Custom Baud Rates
GNU/Linux Method
// Specifying a custom baud rate when using GNU C
cfsetispeed(&tty, 104560);
cfsetospeed(&tty, 104560);
termios2 Method
termios2 struct는 termios struct와 유사하지만 더 많은 기능을 제공한다. 보통 termbits.h에 정의되어 있다.
struct termios2 {
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 */
speed_t c_ispeed; /* input speed */
speed_t c_ospeed; /* output speed */
};
termios2 struct는 termios struct에 c_ispeed와 c_ospeed가 추가되어 있으며 이를 통해 바로 custom baud rate를 설정할 수 있다. termios struct를 이용할 때와 유사하게 baud rate를 제외한 것들을 세팅할 수 있으며 termios struct를 이용할 때에는 file descriptor에 대해 tcgetattr(), tcsetattr()을 이용해 terminal attributes를 read/write하지만 termios2 struct를 이용할 때에는 ioctl()을 이용한다.
// #include <termios.h> This must be removed!
// Otherwise we'll get "redefinition of ‘struct termios’" errors
#include <sys/ioctl.h> // Used for TCGETS2/TCSETS2, which is required for custom baud rates
struct termios2 tty;
// Read in the terminal settings using ioctl instead
// of tcsetattr (tcsetattr only works with termios, not termios2)
ioctl(fd, TCGETS2, &tty);
// Set everything but baud rate as usual
// ...
// ...
// Set custom baud rate
tty.c_cflag &= ~CBAUD;
tty.c_cflag |= CBAUDEX;
// On the internet there is also talk of using the "BOTHER" macro here:
// tty.c_cflag |= BOTHER;
// I never had any luck with it, so omitting in favour of using
// CBAUDEX
tty.c_ispeed = 123456; // What a custom baud rate!
tty.c_ospeed = 123456;
// Write terminal settings to file descriptor
ioctl(serial_port, TCSETS2, &tty);
Saving termios
세팅들을 바꾼 뒤에는 tcsetattr()을 이용해 tty termios struct를 저장한다.
// Save tty settings, also checking for error
if (tcsetattr(serial_port, TCSANOW, &tty) != 0) {
printf("Error %i from tcsetattr: %s\n", errno, strerror(errno));
}
Reading & Writing
1. Reading
Linux 에서 data를 read, write하기 위해서는 buffer를 제공해야 한다. read()함수를 이용하여 reading을 수행한다.
// Allocate memory for read buffer, set size according to your needs
char read_buf [256];
// Read bytes. The behaviour of read() (e.g. does it block?,
// how long does it block for?) depends on the configuration
// settings above, specifically VMIN and VTIME
int n = read(serial_port, &read_buf, sizeof(read_buf));
// n is the number of bytes read. n may be 0 if no bytes were received, and can also be negative to signal an error.
2. Writing
write()함수를 이용해 Linux serial port에 write할 수 있다. 위에서 open()함수를 통해 얻은 file descriptor를 이용하여 write한다.
unsigned char msg[] = { 'H', 'e', 'l', 'l', 'o', '\r' };
write(serial_port, msg, sizeof(msg));
Closing
file descriptor를 close한다.
close(serial_port);