Welcome to My World (www.dgmayor.com)

소프트웨어/자바 GUI & C# 등...

32. 자바 시리얼 포트 통신

dgmayor 2022. 3. 24. 18:08
728x90
 

시리얼 통신이 무엇인가?

다른 말로는 uart통신이라고도 부른다. 

엄밀히 말하면 둘은 동일하지 않지만 보통 시리얼통신 이러면 아 uart통신!이라고 생각한다.

그 uart통신을 자바에서 한번 굴려보도록 하자.

먼저 시리얼 통신을 사용하기 위해서는 라이브러리가 필요하다.

 

http://rxtx.qbang.org/wiki/index.php/Main_Page

 

위의 주소에서 다운로드 받으면 되는데 Download에 들어가서 다운받아주면된다.

 

여기서 stable한 버전의 binary를 설치하도록하자.

 

만약 메이븐레포지터리에서 다운받고 싶다면(혹은 메이븐으로 추가하고싶다면) 메이븐 레포지터리를 확인해도 무방하다.

 

https://mvnrepository.com/search?q=rxtx

 

사용횟수가 그리 많지는 않다. 아무래도 자바로 시리얼통신을 하는 경우가 흔치는 않은것 같다.

대신에 사용하는 방법이 그리 어렵진 않다... 남이 짠 코드만 있으면...(바람직한 태도는 아니다.)

 

이제 설치하는법은 삼척동자도  할 수 있을 정도로 쉽지만 프로젝트에 넣는건 살짝 힘들다.

여기서는 이클립스에서 프로젝트에 넣는 방법에 대해서 짤막히 설명하고 넘어가겠다.

 

 

설치를 하면 파일 내부는 안에처럼 되어있다. 보통의 경우 다운로드를 완료하면 안에있는 jar파일만 사용하면된다.

그러나 시리얼통신은 그렇지 않은데 그 이유는 native한 소스가 필요하기 때문이다.

즉 순수 자바가아니라 실행 파일이 섞인 자바가 필요하다는것이다.

그럼 사용하려면 어떻게 해야하는가? 터미널에서는 조금 코드가 길어지면서 골때리게 되지만 이클립스에서는 간단하다.

이클립스로 사용하는 예제에 대해서 보여주겠다. 일단 당연히 jar파일과 자신의 운영체제에 맞는 라이브러리를 뽑아서 프로젝트에 넣는다.

 

 

이렇게 이쁘게 넣어주면 준비는 끝났는데 설정만 하면된다.

프로젝트를 우클릭한후 properties를 눌러준다.

 

이렇게 java build Path에 jar파일을 추가한다.

그리고 jar파일에 화살표를 클릭을 하자.

 

Native library Location을 선택해준다. 그리고 Edit을 누른다.

 

그 다음 폴더를 선택하면되는데 만약 워크스페이스내에있다면 Workspace를, 아니라면 External Folder를 선택한다.

 

보다시피 필자는 lib에 넣었으므로 lib를 선택하겠다.

다른데 넣었다면 여러분은 다른 파일을 선택해라.

 

다 끝났으면 apply를 누른다.

이제 준비는 끝났고 코드를 치는 일만 남았다.

그런데... 물론 코드를 다 이해하는건 중요하지만 남이 이미 만들어 놓은 코드가 있다.

우리는 일일히 코드를 치지않고 분석하는 선에서 끝내도록 하자.

코드는 크게 세부분으로 나뉜다. 먼저 시리얼을 읽는 SerialReader이다.

 

SerialReader

import java.io.IOException;
import java.io.InputStream;

public class SerialReader implements Runnable {
    InputStream in;

    public SerialReader(InputStream in) {
        this.in = in;
    }

    public void run() {
        byte[] buffer = new byte[1024];
        int len = -1;
        try {
            while ((len = this.in.read(buffer)) > -1) {
                System.out.print(new String(buffer, 0, len));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

보면 알겠지만 코드가 생각보다 매우 쉽다.

외부에 특정 InputStream을 지정해주면 걔로 부터 데이터를 받는다.

SerialReader는 쓰레드(Runnable)로 되어서 계속해서 실행되서 끊임 없이 값을 받는다.

보통 계속해서 신호를 받기때문에 사실상 while문을 빠져나가는 상황은 코드가 꽂혀있는한 없다.

만약 코드가 뽑힌다면 -1이 반환되어 while문은 종료가 되게 될것이다.

게다가 uart통신 특성상 1바이트씩만 받기에 사실 byte는 배열이 아니라도 크게 상관은 없다.

결론은 위의 코드는 화면에 시리얼에서 받은 값을 뿌려주는 역활을 하게 된다.

 

SerialWriter

import java.io.IOException;
import java.io.OutputStream;

public class SerialWriter implements Runnable {
    OutputStream out;

    public SerialWriter(OutputStream out) {
        this.out = out;
    }

    public void run() {
        try {
            int c = 0;
            while ((c = System.in.read()) > -1) {
                this.out.write(c);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

이번에는 보내는쪽의 코드이다. 이역시 OutputStream을 받아온다.

System.in.read는 우리의 표준입력을 날것으로 받아들인다.

받을때도 바이트로 받았는데 보낼때도 바이트로 받는다.

따라서 저기 c는 byte라도 사실 크게 문제는 없다.

while문이 도는동안 사용자의 입력을 계속받게 코드가 짜져있다.

대신 System.in.read는 블로킹 함수이므로 SerialReader마냥 무한대로 돌면서 사용자의 입력을 받지는 않는다.

 

Serial

 

import java.io.InputStream;
import java.io.OutputStream;

import gnu.io.CommPort;
import gnu.io.CommPortIdentifier;
import gnu.io.SerialPort;

public class Serial {
public Serial() {
super();
}

void connect(String portName) throws Exception {
CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(portName);
if (portIdentifier.isCurrentlyOwned()) {
System.out.println("Error: Port is currently in use");
} else {
CommPort commPort = portIdentifier.open(this.getClass().getName(), 2000);

if (commPort instanceof SerialPort) {
SerialPort serialPort = (SerialPort) commPort;
serialPort.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
SerialPort.PARITY_NONE);

InputStream in = serialPort.getInputStream();
OutputStream out = serialPort.getOutputStream();

(new Thread(new SerialReader(in))).start();
(new Thread(new SerialWriter(out))).start();

} else {
System.out.println("Error: Only serial ports are handled by this example.");
}
}
}
}

이제 두 코드를 종합 취합해서 사용할것이다.

전혀 어렵지 않으니 코드의 길이를보고 쫄지 말길 바란다.

먼저 CommPortIdentifier 포트가 실제로 존재하는지, 또한 사용할 수 있는 상태인지확인한다.

아래의 if문은 만약 누군가 사용하고 있다면 연결은 실패한다(포트를 둘이상 점유하는건 불가능하다.)

만약 선점되어 있지않다면 우리가 여는데(open) 2000밀리초의 시간만큼 타임아웃을 가진다.

만약 받은게 시리얼포트라면(instanceof) 보드레이트9600으로 데이터 교환 8비트에 스탑비트1비트,그리고 패리티비트는 없이 보낸다.

모르겠으면 그냥 저대로 사용하면된다. 보드레이트만 맞춰주면된다.

마지막으로 각각의 스트림을 만들어주면된다.

그 다음 쓰레드를 스타트 시키면된다.

 

Main

public class Main {
    public static void main(String[] args) {
        try {
            (new Serial()).connect("COM11");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

이제 마지막으로 포트를 정하고 연결시켜주면된다.

윈도우에서는 무조근 COM으로 시작하지만 리눅스나 맥은 /dev밑에 serial이라는 이름으로 보통 되어 있다.

한번 테스트 해보자.

 

현재 매 클럭마다 hi라는 값을 보내는 아두이노와 연결되어있는 예제이다.

 

 

/////////////////////////

키오스크의 카드 리더기 및 열 프린터기와 연동을 시키려다 보니, 이게 시리얼 통신으로 되어 있다는 걸 알게 되었고....

외부 키오스크를 통해 자동문 제어를 사용할 수 없으면, 개발에 제약이 너무 많아서 카드리더기 및 열 감사 프린터 구입을 요청하고 나니, 시리얼 포트로 운영이 되는 스캐너 과제를 받았다..

예전 zebra 프린터 모듈 만들 때가 생각이 난다.

참고로 리눅스는 환경 설정 파일이 .dll 대신 .so이다.

나중에 계속.....

 

밑 부분은 시리얼 통신이다.

 

앞으로 몇 회에 걸쳐서 시리얼 통신에 대해 말씀 드리려 합니다. (1) 통신의 기본이면서 (2) 값이 매우 저렴하기 때문에 (3) 가장 오래 동안, 그리고 제일 많이 사용되고 있으며 (4) 사용 방법이 간단하고 간편하지만 (5) 또한 개념없이 작성했다가는 개망신 당할 수 있는 것이 시리얼 통신입니다.

개망신이라는 말씀이 좀 지나치겠습니다만 통신이란 혼자서 모두 처리하기 보다는 다른 개발자 또는 이미 만들어진 시스템과 연결하는 경우가 많기 때문에 기본 지식과 개념이 반드시 필요합니다. 또한 개념없는 개발자를 만나서 고생할 때를 대비해서라도 반드시 필요하죠....^^

 

시리얼 포트

  예전에는 시리얼 포트가 모두 25pin 이었습니다. 초창기 P.C.에 달려 있는 시리얼 포트가 모두 25pin 이었거든요. 그러다가 점점 9pin이 사용되어 지다가 지금은 거의 9pin만 사용합니다. 16pin이나 없는데 문제가 없을까요? 없습니다. 실은 컴퓨터 끼리 통신하는데에는 pin 3개만 사용하기 때문이죠. 그럼에도 25pin, 9pin을 사용한 이유는 여러 가지겠지만 외부 모뎀을 사용하기 위해서 그랬지 않았나 생각합니다.

  우선 25pin과 9pin의 모양을 보겠습니다. 자세히 보시면 각 핀 별로 번호가 있습니다. 이 번호를 유의하시고 봐 주세요.

25pin  
9pin  

 

시리얼 통신 케이블 만들기

  통신 테스트를 하시려면 두 대의 시스템을 연결하는 케이블부터 만드어야 겠지요. 이 케이블을 만들기 위해서는 각 핀에 대한 정보부터 아셔야겠습니다.

   1:1 통신인 rs232 통신 외에도 1:N 통신이 422과 485통신이 있지만 여기서는 우선 rs232통신에 대해서만 언급하겠습니다. 또 너무 장황한 내용을 모두 담으면 정신 건강에 이롭지 못합니다.필요한 내용만 적어 보겠습니다. 우선 시리얼 통신에는 3개의 핀만 사용하면 통신이 가능합니다.

  그림을 보시면 아시겠지만 25pin은 2번이 TXD, 3번이 RXD이지만 9pin은 반대로 2번핀이 RXD, 3번핀이 TXD입니다. 이를 염두하고 통신을 위한 케이블을 만들어 보면 아래와 같습니다.

  • 25pin 2개로 만들기, 2번과 3번을 크로스하여 연결하면 되겠습니다.

  • 9pin 2개로 만들기, 2번과 3번을 크로스하여 연결하면 되겠습니다.

  • 25pin과 9pin으로 만들기, 2번과 3번핀의 사용이 서로 반대이므로 그대로 연결하면 되겠습니다.

 

간단 프로그램 작성

  이제 통신을 연결할 케이블까지 만들어 졌으므로 아주 간단한 프로그램을 짜 보겠습니다. 다른 컴퓨터로 "forum.falinux.com"이라는 문자열을 전송해 보죠.

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

int main( void)
{
   int fd;                                                     // (1) 시리얼 통신의 파일 디스크립터 
   struct termios newtio;
   
   fd = open( "/dev/ttyS0", O_RDWR ¦ O_NOCTTY ¦ O_NONBLOCK );  // (2) com1을 open?
   
   memset( &newtio, 0, sizeof(newtio) );
   newtio.c_cflag = B115200;                                   // (3) 통신 속도 115200 
   newtio.c_cflag ¦= CS8;                                      // (4) 데이터 비트가 8bit 
   newtio.c_cflag ¦= CLOCAL ¦ CREAD;                           // (5) 쓰기는 기본, 읽기도 가능하게 
   newtio.c_iflag = 0;                                         // (6) 패리티 사용 안함 
   newtio.c_oflag = 0;
   newtio.c_lflag = 0;
   newtio.c_cc[VTIME] = 0; 
   newtio.c_cc[VMIN] = 1; 
   
   tcflush (fd, TCIFLUSH );
   tcsetattr(fd, TCSANOW, &newtio );
   
   write( fd, "forum.falinux.com", 17);                        // (7) 시리얼로 17자의 문자열을 전송 
   
   close( fd);                                                 // (8) 통신 포트 사용 중지 
   
   return 0; 
}

주석에 간단히 설명을 달았습니다만 모두 이해를 못하셔도 좋습니다. 다음 시간에 요목조목 설명해 드리겠습니다. 중요한 것은  com 포트 1번에 115200 속도로 시리얼 포트를 사용한다는 점입니다. 이 프로그램을 실행하셔서 상대 P.C.에 문자열이 출력되는지를 확인해 보십시오.

어떻게 확인하냐구요? 본 포럼의 자료실에 통신 테스트 프로그램을 올렸습니다. 또한 설명을 자세히 올렸습니다. 참고하시고 상대 P.C.에 실행해 놓으신 후 통신 속도를 115200으로 맞추어 놓고 확인하시면 되겠습니다.

 

시리얼 통신을 이용하기 위해서는 제일 먼저 통신 포트에 대한 사용 권한을 얻어야 합니다. 권한 얻기란 별 다른 것이 아니고, 다른 프로그램이 사용하기 전에 먼저 포트를 열기를 하면 사용 권한을 얻게 됩니다. 말을 거창하게 한듯 합니다만 이 말씀은 포트를 한 번 이상 사용 권한을 얻을 수 없다는 말씀이며, 같은 프로그램이라도 한 번 이상 열기를 할 수 없다는 말씀이 되겠습니다.

유닉스에서는 장치를 파일처럼 사용

유닉스 계열의 운영 체제에서는 시스템에 설치된 모든 하드웨어 장치를 파일 개념을 처리할 수 있으며, 부팅할 때 시스템에 설치된 장치를 확인하고 /dev/ 폴더 밑에 하드웨어 장치 별로 "장치명"의 형식에 따라 파일 형식으로 구성하여 줍니다. 프로그램은 이 장치명을 이용하여 표준 입출력 방법으로 사용할 수 있습니다.

아래의 동영상은 시리얼 포트가 하드웨어 장치이지만 파일 처럼 처리할 수 있음을 보여주는 간단한 예를 소개하고 있습니다.

 

시리얼 통신을 이용하기 위해서는 제일 먼저 통신 포트에 대한 사용 권한을 얻어야 합니다. 권한 얻기란 별 다른 것이 아니고, 다른 프로그램이 사용하기 전에 먼저 포트를 열기를 하면 사용 권한을 얻게 됩니다. 말을 거창하게 한듯 합니다만 이 말씀은 포트를 한 번 이상 사용 권한을 얻을 수 없다는 말씀이며, 같은 프로그램이라도 한 번 이상 열기를 할 수 없다는 말씀이 되겠습니다.

몇가지 장치명을 정리해 보았습니다.

장치명
하드웨어 종류
/dev/fd0 플로피 드라이브, fd0, fd1, fd2, .....
/dev/hda IDE 하드디스크, hda, hdb, hdc, .....
/dev/sda SCSI 하드디스크, sda, sdb, sdc, ....
/dev/cdrom cdrom
/dev/scd cdrom 이 SCSI 일 경우
/dev/dvd DVD 롬
/dev/ttyS0 시리얼 통신 포트, ttyS0, ttyS1, ttyS2, ....

P.C.에는 보통 시리얼 통신 포트가 com1, com2, com3, com4, 4 개가 있습니다만 각 포트별로 I/O 주소는 달라도, com1과 com3, com2와 com4가 같은 IRQ를 사용하고 있기 때문에 DOS에서나 Windows3.1 까지는 com1과 com3 또는 com2와 com4 를 같이 열어 사용할 수 없습니다. 그러나 Windows95이상의 O.S.에서나 리눅스에서는 인터럽트 공유 기술을 이용하여 이와 같은 문제가 없이 모든 포트를 open하여 사용할 수 있습니다.

포트
I/O 주소
IRQ
com1 3f8 4
com2 2f8 3
com3 3e8 4
com4 2e8 3

시리얼 포트의 장치명

디렉토리 /dev 로 이동해서 시리얼 포트의 목록을 보면 아래와 같은 내용이 출력됩니다. 도대체 뭔소리인지 하나씩 알아 보겠습니다.

(1) (2) (3) (4) (5) (6) (7) (8) (9) (10)
crwxrwxrwx 1 root tty 4 64 Jan 1 2006 ttyS00
crwxrwxrwx 1 root tty 4 65 Jan 1 2006 ttyS01
crwxrwxrwx 1 root tty 4 66 Jan 1 2006 ttyS02
crwxrwxrwx 1 root tty 4 67 Jan 1 2006 ttyS03

(1) 접근 권한을 보면 crwxrwxrwx 로 c로 시작하는 것은 장치가 "문자 장치"임을 알려 줍니다. c 가 아닌 b로 시작한다면 "블록 장치"를 말하는데, 예로 하드디스크와 같이 블럭 단위로 읽거나 쓰기를 하는 장치가 되겠습니다.

(5) 의 4는 메이저 장치 번호, (6)의 64, 65, 66 등은 마이너 장치 번호입니다. 우리가 작성하는 프로그램은 하드웨어 장치를 직접 제어하는 것이 아니라 커널을 통해 제어하게 됩니다. 하드웨어를 파일 개념으로 처리할 수 있는 것도 중간에 커널이 가상 파일을 만들어서 제공하기 때문에 가능 한 것입니다.

프로그램에서 하드웨어 장치에 대해 어떤 작업을 커널에게 요청하면, 커널은 메이저 번호를 가지고 어떤 디바이스 드라이버 사용할 지를 결정하게 됩니다. 디바이스 드라이버는 커널로부터 받은 정보 중 마이너 장치 번호를 가지고 자기에게 할당 된 장치 중 어떤 장치를 제어할 지를 결정하게 됩니다.

위의 장치 목록을 보시면 메이저 번호가 모두 4 로 똑 같습니다. 대신에 마이너 번호만 다르죠. 커널은 메이저 번호로 따라 디바이 드라이버를 선택하고 다음 처리를 넘기면 디바이스 드라이버는 마이너 번호를 가지고 어느 장치를 사용할 지를 결정한다는 얘기가 되겠습니다.

이렇게 하드웨어 장치 제어 흐름을 본다면 ttyS0, ttyS1 과 같은 이름은 별로 중요하지 않죠. 중요한 것은 메이저 장치 번호와 마이너 장치 번호가 되겠습니다.

 

삭제된 작치명 복구

ttyS0로 또한 파일 개념이기 때문에 접근 권한을 변경할 수 있으며, 삭제할 수 도 있습니다. 만일 삭제되었다면 아래의 명령으로 다시 생성할 수 있습니다.

]# mknod -m 666 /dev/ttyS0 c 4 64

참고로 다른 예를 하나 더 들면 /dev/null 입니다. /dev/null 장치는 가상 장치로 하수구와 같습니다. 필요 없는 것을 이곳에 쓰기를 하면 모두 실행 성공이지만 이것으로 끝입니다. 디버깅을 할 때나 또는 어떤 명령 실행 후 결과가 필요 없을 경우 출력 파이프를 /dev/null로 설정하면 아무 출력이 이루어 지질 않게 되죠.

그러나 null이 뭐야 하고 지워버리는 경우가 있는데, /dev/null 도 아래와 같은 방법으로 생성할 수 있습니다.

]# mknod -m 666 /dev/null c 1 3

통신 포트 열기와 닫기

장치명에 대해서 파일을 사용하듯 open()과 close()함수를 사용하며 되겠습니다.

int main( void)
{
   int     fd;    <- 통신 포트 사용에 대한 파일 디스크립터

   fd = open( "/dev/ttyS0", O_RDWR | O_NOCTTY | O_NONBLOCK );  <- 통신 포트 열기, 사용 시작

   close( fd); <- 통신 포트 닫기, 사용 완료
   return 0;
}

시리얼 포트를 파일 처럼 처리하기 위하여 파일 디스크립터를 open() 함수를 통해 구하고 있습니다.

  • "/dev/ttyS0" 는 com1 에 해당됩니다.
  • O_RDWR 은 파일 디스크립터 fd를 읽기와 쓰기가 가능하도록 하기 위한 옵션입니다.
  • O_NOCTTY 는  경로명이 터미널 장치일 경우에 사용합니다.
  • O_NONBLOCK 는 경로명의 장치가 순차적인 입축력, 죽, FIFO 이거나 또는 블록단위가 아닌 문자단위 입출력 장치일 경우 사용합니다.

어떻게 포트 열기에 대한 프로그램 예제가 이해 되시나요? 그럼 다음 시간에는 통신 속도에 대해 알아 보겠습니다.

 

bps와 baud

  통신 속도를 말할 때, bbs 와 baud를 혼동하여 사용하는 경우가 많습니다만 차이점을 한번 짚고 넘어 가도록 하겠습니다. bps 는 bit per sec로 1초에 몇 bit를 전송할 수 있는 지를 말하며 baud 는 1초 동안 몇 번 변조를 했는가를 나타내는 말입니다. 초창기에는 이 값이 서로 같았습니다. 전송하는 변조 하나에 비트 하나를 전송했기 때문인데, 기술의 발달로 하나의 변조에 2개, 3개, 4개씩 비트를 전송하는 기술이 나오면서 차이가 생기기 시작했습니다.

  데이터를 전송 또는 수신할 때, 아래와 같이 한번의 변조에 2개의 bit를 묶어서 전송 했다면 1200baud 통신이라고 하더라도 2400bps가 됩니다.

  죽, 아래와 같이 bps 와 baud를 생각할 수 있겠습니다.

bps = baud * 한 번에 변조되는 비트 수

  그러나 P.C. 통신에서는 bps와 baud를 혼동하여 사용해도 별 문제가 없었던 것은 시리얼 통신은 1개의 변조에 1개의 비트를 전송하기 때문에 bps와 baud 값이 서로 같습니다. 또한 모뎀을 사이에 두고 통신한다고 해도 내 PC가 상대 PC와 직접 통신하는 것이 아니라 실제로는 직접 연결된 모뎀하고의 시리얼 통신이기 때문입니다. 참고로 baud는 "보드"가 아니라 "보오"라고 읽으셔야 됩니다.

  baud에는 아래와 같은 종류가 있습니다

110, 300, 120, 2400, 4800, 9600, 1440, 19200, 28800, 38400, 57600, 115200

 

통신 속도 결정

  이제 프로그램에서 보오율을 결정해 보겠습니다. 시리얼 통신에 대한 모든 설정은 termios 구조체를 이용합니다. 이번 예제에는 termios 의 c_cflag 값을 수정하여 보오율을 지정해 주도록 하겠습니다.

int main( void)
{
   int     fd;  
   struct termios newtio;

   fd = open( "/dev/ttyS0", O_RDWR | O_NOCTTY | O_NONBLOCK );

   memset( &newtio, 0, sizeof(newtio) );
   tcflush (fd, TCIFLUSH );
   newtio.c_cflag = B115200;           // 통신 속도 115200
   tcsetattr(fd, TCSANOW, &newtio );

   close( fd);
   return 0;
}

 

start bit

rs232c 통신은 비동기(async) 통신 방식입니다. 즉, 상대방과 내가 통신 라인으로 연결되어 있지만 언제 상대방으로부터 데이터가 전송되어 올지 모릅니다. 예를 들어서 통신 라인에 전압이 0.8v 미만일 때에는 비트 값을 0 으로, 2.4v 이상이면 비트 값을 1로 읽어 들인다고 하겠습니다. 통신 라인을 연결하고 전압을 읽어 보면 0.8v 미만으로 떨어져 있다고 하겠습니다.

이때, 이 값을 상대가 보낸 bit값 0 일가요? 아니면 그냥 연결된 상태 일까요? 아마 둘 중에 하나 일것입니다. 비동기 방식은 이와 같은 문제를 해결하기 위해 데이터를 전송하기 전에 미리 start bit를 보내고 데이터 bit를 전송하게 됩니다.

즉, start bit는 데이터 전송의 시작을 알려 주는 비트가 되겠습니다.

동기(sync)통신은 평상 시에도 동기 신호를 주고 받기 때문에 따로 start bit를 전송하거나 기다릴 필요가 없습니다. 바로 자료를 전송하고 수신 받을 수 잇기 때문에 통신 속도가 빠릅니다. 그러나 하드웨어 구성하는 것이 어렵고 비용이 든다고 하네요.

 

stop bit

stop bit는 이제 데이터 비트를 모두 전송했다라는 뜻의 비트값이 되겟습니다. 또한 stop bit는 한개의 비트로 표현할지, 아니면 2개의 비트로 표현할지를 결정할 수 있는데, 대부분 1 stop bit를 사용합니다.

이와 같이 start bit와 stop bit는 시리얼 통신의 특성 중 하나입니다. 전송되는 데이터 bit 가 8 bit로 설정되어 있어도 실제로는,

[start bit + data bit + stop bit] + [start bit +data bit+stop bit] + ....

이런 모습으로 주고 받게 됩니다.

 

stop bit 설정

start bit는 반드시 사용하므로 프로그램에서는 따로 설정할 것이 없습니다. 대신에 stop bit에 대해서는 1 stop bit를 사용할지 2 stop 비트를 사용할지를 지정합니다.

이전 시간 까지는 (1) 시리얼 포트를 open 하고 (2) 통신 속도를 지정하는 것까지 말씀 드렸습니다. 이번 시간에는 (3) 가장 많이 사용하는 1 stop 비트 설정을 예로 보여 드리고 싶은데, 기본 값이 1 stop bit라 따로 지정할 것이 없습니다. 대신에 2 stop bit 를 사용할 경우의 예를 대신 하겠습니다.

int main( void)
{
   int     fd;  
   struct termios newtio;

   fd = open( "/dev/ttyS0", O_RDWR | O_NOCTTY | O_NONBLOCK );  

   memset( &newtio, 0, sizeof(newtio) );
   newtio.c_cflag = B115200 | RS_2_STOP_BIT;          <-- 생략하면 1 stop bit
   tcflush (fd, TCIFLUSH );
   tcsetattr(fd, TCSANOW, &newtio );

   close( fd);                                      
   return 0;
}

간단하죠. 다음 시간에는 parity 와 data bit 사이즈에 대해서 설명해 드리겠습니다.

 

data bit size

  컴퓨터에서 사용하는 데이터는 1 byte, 8bit를 사용하지만 통신 속도가 느린 시리얼 통신이다 보니 조금이나마 빠르게 통신을 하기 위해 data bit size를 설정하는 것이 있습니다. 예로 영문 ASCII만 사용한다면 굳이 8bit 모두를 주고 받을 필요가 없겠죠. 해서 이럴 때에는 data bit size를 7 bit로 설정합니다.

  또는 주고 받는 데이터가 숫자만 사용한다면 4bit로 대문자 영문 알파벳만 사용한다면 5bit로 주고 받을 수도 있겠습니다. 그러나 이것은 특이한 경우이고 바이너리 데이터나 한글도 주고 받아야 하기 때문에 보통 8bit 를 사용합니다.

 data bit size로 지정하는 상수가 미리 정의되어 있습니다.

data bit size
상수
5 CS5
6 CS6
7 CS7
8 CS8

parity

  parity는 바이트 별로 제대로 수신했는지 확인하는데 사용합니다. parity를 확인하는 방법에는 여러 가지가 있습니다만 예로 even parity를 소개하겠습니다. even parity로 적으면 이해가 어렵습니다만 우리 말로 "짝수 패리티"로 번역해 놓으면 이해가 쉽습니다.  전송하는 데이터 비트 중 1 인 비트를 세어서 짝수 개수이면 뒤에 0 bit를 추가하고 홀수이면 1의 개수가 짝수가 되도록 1 bit를 추가해서 보내 주는 것이 짝수 패리티입니다.

  다시 말씀드려 전송하려는 데이터의 bit 값이 100101 이라면, 1의 개수가 3개라서 홀수입니다. 그러므로 1의 개수를 짝수로 만들기 위해 뒤에 1을 추가하여 전송합니다. 즉, 100101(1)을 보낸다는 것이죠. 100100이면 100100(0) 으로 보내 겠지요. 수신 쪽은 1의 개수를 확인해서 짝수인지 홀수인지를 확인하고 수신한 data bit가 제대로 수신했는지를 확인할 수 있습니다. 홀수 패리티를 사용한다면 반대로 패리티 값이 결정되서 전송되겠죠.

  그러나 패리티를 이용해서 수신 데이터가 이상한 것은 알 수 있어도 어느 비트에 이상이 있는지를 확인할 수가 없습니다. 이런 이유로 보통 사용하지 않습니다. 모든 바이트를 일일이 확인하는 대신에 프로토콜에 아예 체크 바이트를 따로 두고, 전체적으로 확인하는 방법을 사용합니다.

c_iflag
패리티 설정
newtio.c_cflag = PARODD | PARENB 홀수 패리티 사용
newtio.c_cflag = PARENB 짝수 패리티 사용
newtio.c_cflag = 0 패리티 없음

PARODD와 PARENB는 termios.h 에 정의되어 있으며 각각의 값은 아래와 같습니다.

패리티
상수 값
PARODD 512
PARENB 256



data bit size 와 parity 적용

  역시 termio 구조체를 이용합니다.

int main( void)
{
   int     fd;  
   struct termios newtio;

   fd = open( "/dev/ttyS0", O_RDWR | O_NOCTTY | O_NONBLOCK );

   memset( &newtio, 0, sizeof(newtio) );
   newtio.c_cflag = B115200;
   newtio.c_cflag |= CS8;               // 데이터 비트가 8bit, 패리티 없음

   // 만일 홀 수 패리티라면 newtio.c_cflag |= CS8 | PARODD | PARENB;

   newtio.c_iflag = IGNPAR;            // 패리티 오류 무시
   tcflush (fd, TCIFLUSH );
   tcsetattr(fd, TCSANOW, &newtio );

   close( fd);
   return 0;
}

 

이제 예제 프로그램을 전체적으로 분석해 보겠습니다.

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

int main( void)
{
   int fd;
   // 통신 포트를 파일 개념으로 사용하기 위한 디스크립터 입니다.
   // 이 파일 디스크립터를 이용하여 표준 입출력 함수를 이용할 수 있습니다. 

   struct termios newtio;
   // 보오율이나 stop bit 크기 등의 시리얼 통신 환경을 설정하기 위해
   // termios 구조체를 선언했습니다. 

   fd = open( "/dev/ttyS0", O_RDWR ¦ O_NOCTTY ¦ O_NONBLOCK );

   // /dev/ttyS0를 사용하기 위해 open()함수를 사용합니다.
   // O_RDWR은 파일 디스크립터인 fd를 읽기와 쓰기 모드로 열기 위한 지정이며
   // O_NOCCTY와 O_NONBLOCK는 시리얼 통신 장치에 맞추어 추가했습니다.

   memset( &newtio, 0, sizeof(newtio) );

   // 시리얼 통신환경 설정을 위한 구조체 변수 newtio 값을 0 바이트로 깨끗이 채웁니다. 

   newtio.c_cflag = B115200;   // 통신 속도 115200 
   newtio.c_cflag ¦= CS8;      // 데이터 비트가 8bit 
   newtio.c_cflag ¦= CLOCAL;   // 외부 모뎀을 사용하지 않고 내부 통신 포트 사용 
   newtio.c_cflag ¦= CREAD;    // 쓰기는 기본, 읽기도 가능하게 
   newtio.c_iflag = 0;         // parity 비트는 없음
   newtio.c_oflag = 0;
   newtio.c_lflag = 0;
   newtio.c_cc[VTIME] = 0; 
   newtio.c_cc[VMIN] = 1; 

   tcflush (fd, TCIFLUSH );
   tcsetattr(fd, TCSANOW, &newtio );   // 포트에 대한 통신 환경을 설정합니다. 
   
   write( fd, "forum.falinux.com", 17);
   // 표준 입출력 함수를 이용하여 시리얼 포트로 17자의 문자열을 전송합니다.
   
   close( fd);
   // 통신 포트를 닫아 사용을 중지합니다. 
   
   return 0; 
}

이번 시간에는 시리얼 통신에서 자료를 수신하는 부분을 구현하려 합니다. 그러나 송신 보다 수신하는 부분은 생각할 점이 있습니다. 전송이야 이쪽에서 필요할 때 보내기만 하면 되기 때문에 “언제라는” 시간적인 문제가 없습니다. 그러나 수신은 자료가 언제 올지를 모르죠. 기다려야 한다는 것인데, 자료가 올 때까지 마냥 시리얼 포트만 쳐다 보고 있을 수 없습니다. 다른 일도 처리 해야죠. 해야할 일이 산더미처럼 쌓였는데, 마냥 포트만 쳐다 볼 수 없습니다.

 

이럴 때 쉽게 생각할 수 있는 것이 일을 처리하는 중에 잠시잠시 포트를 확인하는 방법입니다. 가령 예를 들어서 아래와 같이 하는 것이죠.

while( 1)
{
  // 다른 업무를 실행
  if 0 < read( fd, buf, BUF_MAX_SIZE)
  {
    // 수신 자료를 처리
  }
}

물론 이와 같은 방법도 좋습니다만 ?자료 수신 이외의 이벤트 처리, 예로 통신에 에러가 발생하지 않았는지 등을 확인을 위해서는 또 다른 확인 루틴을 작성하고 if 절을 추가해야 합니다. 그러나 무엇보다도 read()함수가 block되 버리면 루틴 자체가 block되어 버리는 매우 큰 문제를 가지고 있습니다.

 

이럴 때 사용하는 것이 POLL입니다.

 

poll()

POLL은 확인하고 싶은 여러 가지 사건( 이하 event)를 미리 등록해 놓고 그 사건들이 발생했는지 확인할 수 있는 편리한 방법을 제공해 줍니다. POLL을 이용한 작업 진행 과정을 정리해 보면,

  1. 체크하고 싶은 여러 event를 등록
  2. poll() 함수를 호출하면
  3. event가 발생하면 poll() 함수 호출 후에 바로 복귀하지만,
  4. 발생된 event 가 없으면 하나 이상이 발생할 때 까지 time-out 시간 만큼 대기하게 됩니다.
  5. event가 발생면 해당 event의 배열 아이템의 값이 변동이 되는데,
    이 변동을 확인하여 어떤 event 가 발생했는지를 알 수 있습니다.

장황한 설명 보다는 먼저 간단한 예를 보여 드리고 그 후에 자세한 설명을 드리도록 하겠습니다. 예제는 이해를 돕기 위해 상수를 사용하지 않았고, 함수로 분리하지 않았습니다.

예제

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/poll.h>
#include <termios.h>                   // B115200, CS8 등 상수 정의
#include <fcntl.h>                     // O_RDWR , O_NOCTTY 등의 상수 정의

int main( void)
{        
   int    fd;
   int    ndx;
   int    cnt;
   char   buf[1024];
   struct termios    newtio;
   struct pollfd     poll_events;      // 체크할 event 정보를 갖는 struct
   int    poll_state;

   // 시리얼 포트를 open

   fd = open( "/dev/ttyS0", O_RDWR | O_NOCTTY | O_NONBLOCK );        // 디바이스를 open 한다.
   if ( 0 > fd)
   {        
      printf("open error\n");
      return -1;
   }

   // 시리얼 포트 통신 환경 설정

   memset( &newtio, 0, sizeof(newtio) );
   newtio.c_cflag       = B115200 | CS8 | CLOCAL | CREAD;
   newtio.c_oflag       = 0;
   newtio.c_lflag       = 0;
   newtio.c_cc[VTIME]   = 0;
   newtio.c_cc[VMIN]    = 1;
   
   tcflush  (fd, TCIFLUSH );
   tcsetattr(fd, TCSANOW, &newtio );
   fcntl(fd, F_SETFL, FNDELAY); 


   // poll 사용을 위한 준비
   
   poll_events.fd        = fd;
   poll_events.events    = POLLIN | POLLERR;          // 수신된 자료가 있는지, 에러가 있는지
   poll_events.revents   = 0;


   // 자료 송수신

   while ( 1)
   {
      poll_state = poll(                               // poll()을 호출하여 event 발생 여부 확인     
                         (struct pollfd*)&poll_events, // event 등록 변수
                                                   1,  // 체크할 pollfd 개수
                                                1000   // time out 시간
                       );

      if ( 0 < poll_state)                             // 발생한 event 가 있음
      {     
         if ( poll_events.revents & POLLIN)            // event 가 자료 수신?
         {
            cnt = read( fd, buf, 1024);
            write( fd, buf, cnt);
            printf( "data received - %d %s\n", cnt, buf);
         }
         if ( poll_events.revents & POLLERR)      // event 가 에러?
         {
            printf( "통신 라인에 에러가 발생, 프로그램 종료");
            break;
         }
      }
   }
   close( fd);
   return 0;
}

  예제를 보시면 struct pollfd를 사용했습니다. struct pollfd의 내용을 보면 아래와 같습니다.

struct pollfd
{
  int fd;             // 대상 파일 디스크립터
  short events;   // 발생된 이벤트
  short revents;   // 돌려받은 이벤트
};

  1. fd는 감시 대상인 디스크립터, 즉 핸들이 되겠습니다.
  2. events는 체크하고 싶은 event의 모음입니다. 여기서 체크가 가능한 것은 아래와 같습니다.
  • #define POLLIN 0x0001 // 읽을 데이터가 있다.
  • #define POLLPRI 0x0002 // 긴급한 읽을 데이타가 있다.
  • #define POLLOUT 0x0004 // 쓰기가 봉쇄(block)가 아니다.
  • #define POLLERR 0x0008 // 에러발생
  • #define POLLHUP 0x0010 // 연결이 끊겼음
  • #define POLLNVAL 0x0020 // 파일지시자가 열리지 않은 것 같은, Invalid request (잘못된 요청)
  1. revents 는 event 발생 여부를 bit 별로 갖는 값입니다.

 

  자, 이제 예제를 보겠습니다.

struct pollfd poll_events; // 체크할 event 정보를 갖는 struct

poll()을 사용하기 위한 변수를 선언했습니다. poll_events 에는 감시 대상인 디스크립터와 어떤 event를 감시할지 결정해서 bit 값으로 지정해 줄 것입니다.

int poll_state;

poll()이 수행한 결과값을 갖습니다. 이 값은 반드시 체크해야 하는데, 아래와 같은 반환값을 갖습니다.

 

poll() 수행 후 반환 값
반환 값 설명
음수 반환 값이 음수라면 치명적인 에러가 발생한 것입니다. 한번 이렇게 음수로 에러가 발생하면 이후 계속 음수값이 날라 옵니다. 거의 대부분 프로그램을 다시 실행해야 됩니다. 반드시 체크해야 겠지요.
0 지정한 대기시간, time-out이 지나도록 발생한 event가 없습니다.
양수 event 가 발생했습니다.

poll_events.fd = fd;

감시 대상인 디스크립터를 지정합니다.

poll_events.events = POLLIN | POLLERR; // 수신된 자료가 있는지, 에러가 있는지

체크하고 싶은 event에 대해 비트값으로 설정하여 지정해 줍니다.

poll_events.revents = 0;

revents를 0 으로 청소해 주고요.

poll_state = poll(                               // poll()을 호출하여 event 발생 여부 확인     
                  (struct pollfd*)&poll_events, // event 등록 변수
                                             1,  // 체크할 pollfd 개수
                                          1000   // time out 시간
                );

체크할 event 정보를 넘겨 줍니다. 1은 체크할 pollfd 개수입니다. 이 예제를 가지고는 pollfd의 개수가 왜 필요한지 이해가 안되시죠. 예제에는 감시하는 디스크립터가 한 개이지만 프로그램에 따라서는 한번에 여러 개의 디스크립터를 관리할 경우가 많습니다.

 

관리할 디스크립터가 많을 때, 각각을 변수로 처리하는 것 보다 배열로 처리하는 것이 편하겠죠. 이래서 poll() 이 편리합니다. poll()은 변수 하나 외에도 배열을 받을 수 있으며, 한번의 호출로 모든 event 발생 여부를 확인할 수 있어 매우 편리합니다.

 

 

 

1000은 time-out 시간으로 발생한 event 가 없을 경우 poll()은 event가 발생할 때 까지 time-out 시간 동안 대기하게 됩니다.

이 시간 값은 msec로 1000은 1초를 의미합니다. 0 이면? 바로 복귀합니다. -1 이면? evevnt 가 발생할 때 까지 기다리게 됩니다. 즉, block되 버리죠.

if ( 0 < poll_state)

poll() 함수 결과가 양수라면 event 가 발생한 것입니다.

if ( poll_events.revents & POLLIN)

발생한 event 중에 POLLIN 이 있는 지를 확인합니다. POLLIN에 해당되는 bit 가 1로 세트되어 있다면 POLLIN으로 AND한 값은 0 이 아닐 것입니다. 이렇게 비트값을 확인하여 event 발생 여부를 확인할 수 있습니다.

cnt = read( fd, buf, 1024);
write( fd, buf, cnt);
printf( "data received - %d %s\n", cnt, buf);

자료 수신 event 가 발생했으므로 fd로부터 자료 값을 읽어 들이고, 예제 테스트를 위해 다시 그 자료를 전송합니다. 또한 화면에도 수신한 자료를 뿌리고 있죠.

예제에 대한 설명은 여기 까지 드리겠습니다. 어떻게 자료 송신부터 수신까지 이해가 되시나요? 다음 시간에는 지금까지의 예제를 좀더 발전시켜서 한번에 여러 포트와 통신하는 방법을 말씀드리겠습니다. 조금씩 조금씩 발전 시켜 나가는 거죠.....^^

 

긴 글을 읽어 주셔서 감사합니다.

728x90