15 min read

시리얼 포트 통신

시리얼 통신. 요즘 세상에 어디서 시리얼 통신을 쓰겠냐 하겠지만 의외로 많이 쓰인다. 외부장치 연결에 쓰는 USB는 Universal Serial Bus의 약자고, 하드디스크나 SSD 연결에 쓰는 S-ATA도 Serial ATA의 약자이다.

물론 여기서 말하는 시리얼 포트는 UART 시리얼 통신 내지는 RS-232 시리얼 통신을 의미하한다. 여기에 사용하는 물리 포트로 DE-9 포트를 사용할 것이다. 또는 0.1인치 4핀 헤더를 사용하거나.

DB-9 Header
SparkFun RS232 Shifter

사실 엄밀히 말하자면 UART는 비동기 통신을 하는 논리적 방식이고, RS-232는 전기신호를 전달하고 주변장치를 제어하는 물리적 방식이다. DE-9는 9핀 케이블을 접속하기 위한 하드웨어 커넥터이고. 하지만 본 문서에서는 이 셋을 엄밀히 구분하지 않고 그냥 뭉뚱그려서 사용한다.

여하튼. RS-232는 1960년대에 표준이 정해져 있고, 속도는 빨라봐야 115.2kbps 정도이다. 대신 구현이 간편해서 2020년이 가까워오는 현재에도 많이 사용하고 있다. 라즈베리 파이나 아두이노 같은 임베디드 장난감을 만져봤다면 UART 4핀을 많이 봤을 것이고, 아직도 많은 산업 장비가 RS-232 시리얼 포트로 연결된다. 블루투스 데이터 채널도 내부적으로는 시리얼 포트를 에뮬레이트 한다.

이 글에서는 RS-232의 동작방식, 특히 흐름제어 관련된 이야기를 할 것이다. RS-232에서는 데이터 전송용 2핀, 흐름제어 4핀, 검출 2핀, 접지 1핀으로 총 9핀을 사용하는데, 라즈베리 파이 연결할 때에는 3핀만 연결해도 시리얼통신이 가능하다. 어째서 가능한지, 그럼 나머지 핀은 왜 존재하며 어떻게 사용하는지 등에 대해 이야기 해 보자.

RS-232 전압

RS-232의 Voltage Swing은 +-15V에 달한다. 일반적인 UART가 3.3V 내지는 5V 전압에서 작동하는걸 생각하면 어마어마하게 높은 전압이다. Twisted pair 같은 것을 사용하지 않고 전압을 쌩으로 날리기 때문에, 잡음 및 거리를 효율적으로 잡기 위해 그런 것이다. 대충 3V 정도 노이즈는 씹어버릴 수 있으므로, 쌩 전선으로 15미터 거리에서 통신이 가능하다.

[-15V,  -3V] = Data Mark bit (1)  = Control Deasserted
[ -3V,  +3V] = Invalid voltage
[ +3V, +15V] = Data Space bit (0) = Control Asserted

이를 알려주는 이유는, RS-232 핀을 그대로 UART 핀에 쳐박지 말라는 것이다. 몇 만원을 주고 산 장난감이 불타버리는 꼴을 보고 싶지 않다면 말이다. USB-UART 컨버터를 쓰거나, 레벨 시프터 회로를 쓰도록 하자.

RS-232 핀아웃

PIN Abbr. DIR Description
1 CD <-- Carrier Detect
2 RxD <-- Received Data
3 TxD --> Transmitted Data
4 DTR --> Data Terminal Ready
5 GND <-> Ground
6 DSR <-- Data Set Ready
7 RTS --> Request To Send
8 CTS <-- Clear To Send
9 RI <-- Ring Indicator

DIR은 신호가 전달되는 방향을 의미한다. 왼쪽이 Data Terminal Equipment = 터미널, 오른쪽이 Data Circuit-terminating Equipment = 모뎀을 의미한다.

왜 용어가 이 모양이냐면, RS-232가 정의된게 1960년대이기 때문이다. 당시에는 PC도 없었고 텔레타이프 내지는 터미널을 이용하여 메인프레임과 통신하였기 때문이다. 이 때 전화선을 이용한 통신을 하려면 터미널과 모뎀을 연결해야 했는데, 그 연결하는 방법이 RS-232이다. 이러한 배경 설명이 없으면 대체 왜 저런 기능이 필요한데? 라는 질문부터 나올 것이다.

2-3번, 4-6번, 7-8번 핀이 서로 대응하는 기능이다. 1, 9번은 각각 개별로 작동한다.

하드웨어 흐름 제어

DTR, DSR, CD

애당초 RS-232가 터미널과 모뎀 사이의 통신을 위한 것이었다. DTR과 DSR은 터미널에서 모뎀 자체를 제어하는 신호이다.

먼저 터미널은 모뎀 사용이 필요하게 되면 DTR=HIGH로 설정한다. 모뎀은 이를 인식하고 초기화 절차등을 수행하고, 전화기를 든 상태로 대기하며, DSR=HIGH로 설정하여 터미널에 통신 준비가 완료되었음을 알리게 된다. 이제 터미널에서 모뎀 제어명령을 내려서 전화를 걸 수 있게 된다.

상대방과 전화가 연결되면 CD=HIGH 로 설정된다. 글자 그대로 통신망(Carrier)이 잡혔다는 이야기이다.

상대방이 전화를 끊었거나, 또는 기타 이유로(누가 전화선을 뽑았다던지) 전화가 끊어진 경우, CD=LOW로 설정되므로 터미널에서도 전화가 끊겼음을 알 수 있다. 물론 NO CARRIER 데이터를 전달하기도 하지만, 이게 모뎀이 보낸건지 상대방이 보낸건지 알 수 없으니 CD=LOW를 확인하는 것이 가장 정확하다. 모뎀이 죽었다면 DSR=LOW로 설정될 것이다. 반대로 터미널에서 전화를 끊으려는 경우 DTR=LOW로 설정하면 된다. 이 경우 모뎀은 2초 후에 전화를 끊는다.

현재에는 DTR-DSR을 이용한 흐름제어는 잘 사용하지 않는다. 상대방 장비가 연결되었는지, 활성화되어 있는지 점검하는 목적으로 사용하는 정도다.

RTS, CTS

만약 하드웨어 흐름 제어를 이야기한다면 이 RTS와 CTS를 이야기하는 것이다.

사실 RTS-CTS는 모뎀이 븅신이라 터미널-모뎀 사이에 반이중 통신(Half-duplex)을 사용하기 위해서 도입된 제어 신호이다. 기본적으로는 수신 모드로 RxD 채널 사용하다가, 컴퓨터에서 모뎀으로 데이터를 보내야 하는 경우 RTS=HIGH로 설정하여 전송 요청(Request To Send)을 한다. 모뎀에서 이를 확인하고 전송 모드로 전환한 뒤 CTS=HIGH로 설정하여 전송 요청을 수락(Clear To Send)하면 비로소 TxD 채널을 쓸 수 있는 것이다. 데이터 전송이 끝나면 RTS=LOW로 전환되고, 그러면 모뎀은 다시 수신 모드로 전환한 뒤 CTS=LOW로 전환하고 RxD 채널을 사용하게 된다.

하드웨어 성능이 좋아지면서, 터미널-모뎀 사이에 전이중 통신(Full-duplex)을 사용할 수 있게 되었고 이에 따라 RTS-CTS는 역할이 달라지게 된다. 이 때 RTS는 RTR(Ready To Receive)로 이름이 뒤바뀐다.

터미널의 버퍼가 넉넉하여 데이터를 수신할 수 있다면 RTR=HIGH로 두어 수신 가능함(Ready To Receive)을 알린다. 그러면 모뎀은 전화선으로 날아오는 데이터를 바로 터미널로 꽂아준다. 터미널의 버퍼가 많이 차서 데이터를 수신할 수 없다면 RTR=LOW로 두어 수신 불가능하다고 알린다. 그러면 모뎀은 상대방에게 데이터 보내지 말라고 연락하여 속도를 조정할 수 있는 것이다.

같은 매커니즘이 모뎀에도 적용된다. 통신채널이 원활하고 모뎀 버퍼가 넉넉하다면 CTS=HIGH로 두어 데이터를 전송 가능함(Clear To Send)을 알린다. 그러면 터미널은 데이터를 필요한 만큼 전송할 수 있다. 만약 모뎀 버퍼가 많이 차거나, 또는 전화 상대방이 데이터를 보내지 말라고 연락했다면 CTS=LOW로 설정한다. 그러면 터미널은 데이터 전송을 중단하고 기다릴 것이다. 멋지게 흐름 제어를 수행할 수 있는 것이다.

이러한 하드웨어 흐름 제어는, 여하튼 케이블이 추가로 연결되어야 하는 만큼 케이블 가격이 상승하는 결과를 가져온다. 그리고 흐름 제어 신호를 처리하는 별도의 로직을 추가해야 하는 만큼 하드웨어도 복잡해진다.

소프트웨어 흐름 제어

TxD, RxD

TxD는 터미널에서 모뎀으로 데이터를 전송하는 채널이고, RxD는 반대로 모뎀에서 터미널로 데이터를 전송하는 채널이다.

이 채널을 이용해서도 흐름 제어를 수행할 수 있다. 예를 들어 모뎀 버퍼가 많이 찼다면 모뎀은 컴퓨터로 특정한 신호를 보내어 전송을 멈추라고 요청할 수 있다. 그리고 버퍼가 비면 다시 특정한 신호를 보내어 전송을 재개하라고 요청할 수 있다. 터미널 또한 마찬가지이다.

이 때 사용하는 특수문자가 0x11 Xon, 그리고 0x13 Xoff 이다. X는 Transmitter를 의미하므로, Xoff는 전송중단, Xon은 전송재개를 의미한다.

일반적으로 ASCII 문자열만을 전송하는 시스템이라면 제어문자를 사용하여 흐름 제어를 하더라도 별 문제가 없지만, 이진 데이터를 전송하는 시스템이라면 데이터 자체에서도 0x11, 0x13이 나올 수 있으므로 골때리는 상황이 발생하게 된다. 따라서 소프트웨어 흐름 제어는 사용범위가 좁다. 데이터를 적절히 인코딩 하여 ASCII 안으로 욱여넣던지, Escape sequence를 사용하던지, 아니면 하드웨어 흐름 제어를 사용해야 한다.

널모뎀 케이블

위에서 기술한 하드웨어, 소프트웨어 흐름 제어는 어디까지나 터미널-모뎀 사이의 제어를 위한 것이다. 그런데 우리가 하려는 것은 결국 컴퓨터와 컴퓨터 사이에서 데이터 통신이다. 전화망을 이용해야 할 만큼 장거리에 떨어져 있다면 어쩔 수 없지만, 컴퓨터 두 대가 옆에 붙어 있다면 굳이 모뎀을 두고 전화비를 납부하면서 통신을 써야 할까? 아닐 것이다. 모뎀 빼고 컴퓨터 두 대를 어떻게 잘 연결하면 되겠지.

그래서 나온 것이 널모뎀 케이블이다. 글자 그대로 모뎀이 없는(Null-modem) 케이블이다. 물론 지금은 대부분 이더넷 네트워크를 사용하겠지만, 이 글을 읽는 사람이라면 RS-232 통신을 하려는 (아니면 해야 하는) 것이니까, 넘어가자.

소프트웨어 흐름 제어

소프트웨어 흐름 제어를 쓸 것이라면 케이블 3개만 연결하면 된다. TxD, RxD 그리고 GND. 라즈베리 파이나 아두이노를 컴퓨터와 연결할 때 보통 이렇게 연결할 것이다. 흐름제어를 할 필요가 거의 없기 때문이다.

이 때 내가 보내는 데이터는 상대방이 받아야 하고, 상대방이 보내는 데이터는 내가 받아야 한다. 따라서 내 TxD는 상대방의 RxD로, 상대방의 TxD는 내 RxD로 연결해야 한다.

2 RxD ←────── TxD 3
3 TxD ──────→ RxD 2
5 GND ←─────→ GND 5
4 DTR         CD  1
6 DSR         DSR 6
1  CD         DTR 4
7 RTR         CTS 8
8 CTS         RTR 7
9  RI         RI  9

소프트웨어 흐름 제어 + 사기치기

이제 컴퓨터와 장비를 연결한다고 생각해 보자. 컴퓨터야 소프트웨어 흐름제어를 쓴다는걸 아니까 DTR-DSR이 상태가 어떻건, RTR-CTS가 어떻건 그냥 쌩까고 데이터를 때려박는다. 그런데, 장비는 이걸 설정할 방법이 없다. 그렇다면 어떻게 해야 할까.

약간의 사기를 쳐 보자. 우선 DTR을 생각해 보자. 컴퓨터에서 DTR=HIGH로 설정하고서 기대하는 신호는 어떤걸까. 모뎀이 작동하고 전화선이 연결되는 것이므로 DSR=HIGH 그리고 CD=HIGH 이다. DTR=LOW로 설정하면? 더이상 통신 안 할 것이므로 DSR=LOW로 꺼져야 하고, CD=LOW로 전화가 끊겨야 한다. 어? 그럼 그냥 DTR=DSR=CD 로 묶어버리면 되겠네?

다음 사기. 내가 RTR을 HIGH로 설정하고 기대하는 신호는 어떤 것일까. 사실 RTR=HIGH로 설정했다는 것은 데이터를 받을 준비가 되었다는 것이며 딱히 기다리는 신호는 없다. 반면 CTS=HIGH가 되어야 데이터를 보낼 수 있다. 그럼 둘을 묶어버리면? 데이터를 받을 준비가 되었다면 보낼 수도 있게 된다. RTR=LOW가 되면 CTS=LOW가 되므로 받지도 보내지도 못한다. 상대방이야 무작정 계속 데이터를 보낼 것이지만, RTR=LOW일 때 들어오는 데이터는 그냥 쌩까게 된다.

이제 9개 핀이 모두 제대로 된 입출력을 갖게 되었다. 물론 실제로 연결되는 케이블은 여전히 3개 뿐이므로, 하드웨어 흐름 제어는 제대로 작동하지 않는다. 오롯이 소프트웨어 흐름 제어에 의존해야 한다.

2 RxD ←────── TxD 3
3 TxD ──────→ RxD 2
5 GND ←─────→ GND 5
4 DTR ──┐ ┌─→ CD  1
6 DSR ←─┤ ├─→ DSR 6
1  CD ←─┘ └── DTR 4
7 RTR ──┐ ┌─→ CTS 8
8 CTS ←─┘ └── RTR 7
9  RI         RI  9

하드웨어 흐름 제어 - RTR-CTS

다시. 이번에는 하드웨어 흐름 제어를 쓸거다. 쓸건데 RTR-CTS 제어만 쓸 거다.

내가 받을 준비가 되었다는 신호는 상대방 입장에서는 보내도 좋다는 신호이다. 따라서 내 RTR을 상대방의 CTS에 묶어준다. 그 반대도 같다. DTR-DSR-CD를 묶어서 사기치는건 위와 같다.

일반적으로 널모뎀에서 하드웨어 흐름제어를 이용한다면 이러한 셋업을 사용하게 될 것이다. 케이블이 5개 연결되는 것을 볼 수 있다.

2 RxD ←────── TxD 3
3 TxD ──────→ RxD 2
5 GND ←─────→ GND 5
4 DTR ──┐ ┌─→ CD  1
6 DSR ←─┤ ├─→ DSR 6
1  CD ←─┘ └── DTR 4
7 RTR ──────→ CTS 8
8 CTS ←────── RTR 7
9  RI         RI  9

하드웨어 흐름 제어 - 풀버전

이번에는 상대방 기기의 상태까지 체크해 가면서 쓸 것이다. 내가 DTR=HIGH로 설정하면 상대방 입장에서는 모뎀이 셋업되고 전화까지 연결되었다는 의미이다. 따라서 내 DTR을 상대방의 DSR과 CD에 묶어준다. 반대방향도 같다.

다만 이 셋업은 양쪽의 장비가 모두 DTR-DSR-CD를 이용한 흐름 제어까지 지원해야 하므로, 거꾸로 호환성이 떨어지는 결과를 가져온다. 암만 한 쪽이 DTR=HIGH로 설정하고 DSR을 기다리고 있은들, 반대쪽 장비가 DTR=LOW로 방치하면 통신이 실패하는 것이다.

2 RxD ←────── TxD 3
3 TxD ──────→ RxD 2
5 GND ←─────→ GND 5
4 DTR ────┬─→ CD  1
6 DSR ←─┐ └─→ DSR 6
1  CD ←─┴──── DTR 4
7 RTR ──────→ CTS 8
8 CTS ←────── RTR 7
9  RI         RI  9

RI

모뎀은 전화선에 연결되고, 그렇다면 상대방이 나에게 전화를 할 수도 있는 것이다. 우리집에 전화가 오면 전화에서 벨이 울리듯, 모뎀이 전화를 받게 되면 RI=HIGH로 설정되어 터미널에 신호가 간다.

그런데, 전화망을 이용한 사설 서버를 구축하는게 아니라면, RI를 쓸 일은 없을 것이다. 널 모뎀에서도 쓸 일이 없으므로 그냥 오픈 커넥션으로 열어두게 된다. 혹시나 양쪽을 연결하는 일이 없길 바란다.