콘텐츠로 이동
무료 평가 시작

가상 시리얼 포트의 개념

PDComm 가상 시리얼(virtual serial)은 운영체제와 응용 프로그램이 실제 시리얼 포트로 다루는 포트를 만듭니다. 포트 사이의 케이블과 버스는 소프트웨어 엔진이 흉내 냅니다. 이 페이지는 가상 포트로 시험할 때 알아야 할 개념을 설명합니다.

가상 시리얼의 실행 단위를 랩(lab)이라고 합니다. 랩은 JSON 파일 하나로 정의합니다.

  • 노드(node): 이름이 있는 포트 하나입니다. 노드마다 UART 문자 형식, 흐름 제어, 수신 FIFO, 결함 설정을 둡니다.
  • 매체(medium): 노드를 잇는 케이블이나 버스입니다. 널 모뎀 케이블, 루프백 플러그, RS-422/RS-485 버스를 고릅니다.
  • 포트 바인딩(port binding): 노드를 응용 프로그램이 여는 실제 끝점(endpoint)에 연결합니다. 의사 터미널, 포트 드라이버, TCP 서버 중 하나를 고릅니다.
/tmp/pdcomm-plc ◀──▶ node plc ═══ null_modem ═══ node hmi ◀──▶ /tmp/pdcomm-hmi
19200 8E1 19200 8E1

위 그림에서 응용 프로그램 A는 /tmp/pdcomm-plc를, 응용 프로그램 B는 /tmp/pdcomm-hmi를 엽니다. 두 포트 사이의 널 모뎀 케이블은 랩이 흉내 냅니다.

시리얼 포트는 바이트를 문자(character) 단위로 보냅니다. 문자 하나는 시작 비트 1개, 데이터 비트 58개, 패리티 비트 01개, 정지 비트 1·1.5·2개로 이루어집니다. 예를 들어 8E1(데이터 8, 짝수 패리티, 정지 1)은 문자당 11비트입니다. 19200bps에서 문자 시간(character time)은 약 0.57ms입니다.

엔진은 노드마다 다음 값을 사용합니다.

설정 허용 값 기본값
baud 1~16000000 9600
data_bits 5~8 8
parity none, odd, even, mark, space none
stop_bits "1", "1.5", "2" (문자열) "1"

**진행 방식(pacing)**은 랩 전체에 적용합니다.

  • realistic: 문자마다 문자 시간을 씁니다. 실제 회선처럼 늦게 도착합니다. Modbus RTU처럼 문자 사이 간격이 의미 있는 프로토콜은 이 방식을 사용합니다.
  • instant: 순서는 지키고 지연 없이 전달합니다. 논리만 빠르게 시험할 때 사용합니다.

수신 측 UART는 자기 설정으로 비트를 읽습니다. 양쪽의 속도, 데이터 비트, 패리티가 다르면 엔진은 실제 수신기처럼 깨진 바이트와 프레이밍(framing) 오류, 패리티(parity) 오류를 보고합니다. 그래서 설정 불일치를 그대로 재현할 수 있습니다.

실제 16550 계열 UART는 수신 문자를 FIFO에 모았다가 한꺼번에 넘깁니다. 엔진도 같은 방식으로 문자를 전달합니다. 기본값은 문자 14개가 모이거나 문자 시간 4개 동안 회선이 조용할 때 전달하는 것입니다("rx_fifo": {"trigger": 14, "idle_chars": 4}). "rx_fifo": null로 지정하면 문자마다 따로 전달합니다. 실제 UART나 USB 변환기보다 잘게 나누어 전달하므로 수신 코드의 부담 시험에 씁니다.

포트는 RTS와 DTR을 출력하고 CTS, DSR, DCD, RI를 입력으로 봅니다. 널 모뎀 케이블은 기본으로 한쪽의 RTS를 상대의 CTS에, DTR을 상대의 DSR과 DCD에 잇습니다. 매체의 pins로 다른 배선을 지정할 수도 있습니다.

흐름 제어(flow control)는 none, rts_cts(하드웨어), xon_xoff(소프트웨어) 중 하나입니다. 응용 프로그램이 포트에서 설정을 바꾸면 랩도 그 설정을 따릅니다. 브레이크(break) 신호도 전달합니다.

kind 실제 배선 필드
null_modem RS-232 크로스 케이블, 또는 점대점 RS-422 a, b, pins(선택)
loopback 포트 하나에 꽂은 루프백 플러그 node, pins(선택)
four_wire RS-422 또는 4선식 RS-485. 마스터가 모든 슬레이브에 보내고 슬레이브는 반환 쌍을 공유 master, slaves
two_wire 2선식 RS-485. 모든 포트가 반이중 쌍 하나를 공유 nodes

노드 하나는 매체 하나에만 연결합니다. 2선식 버스에는 노드가 2개 이상, 4선식 버스에는 슬레이브가 1개 이상 필요합니다.

2선식 RS-485 버스 (two_wire)
master drive1 drive2
│ │ │
════╪═════════════╪═════════════╪════ A/B 쌍 하나 (반이중)

공유 버스에서는 한 번에 한 포트만 송신해야 합니다. 노드의 attach로 송신 드라이버를 켜는 조건을 정합니다.

  • "driver_enable": "always": 항상 송신합니다. 널 모뎀과 루프백의 기본값입니다.
  • "driver_enable": "auto": 방향을 자동으로 바꾸는 트랜시버입니다. 버스 노드의 기본값입니다.
  • "driver_enable": {"rts": {"asserted": true}}: RTS가 지정한 레벨일 때만 송신합니다. 응용 프로그램이 RTS로 방향을 제어하는 배선입니다.

"echo": true는 송신 중에도 수신기가 켜져 있어 자기 문자를 다시 받는 트랜시버를 흉내 냅니다. 두 포트가 동시에 송신하면 엔진은 충돌(collision)로 기록합니다.

포트의 rts_toggle은 UART가 송신할 때마다 RTS를 자동으로 올리고 내리는 기능입니다. Linux의 RS-485 모드(TIOCSRS485)와 Windows의 RTS_CONTROL_TOGGLE이 이렇게 동작합니다.

결함(fault)은 두 가지 방법으로 넣습니다.

  • 확률 결함: 노드의 faults에 백만분율(ppm)로 지정합니다. drop_ppm, flip_ppm, parity_ppm, framing_ppm, overrun_ppm, noise_ppm과 전달 지연 latency_ns, jitter_ns가 있습니다. 같은 seed는 항상 같은 결과를 재현합니다.
  • 1회 결함: 실행 중인 랩에 명령으로 넣습니다. 그 노드가 다음에 받는 문자 하나에 적용합니다. "drop"(버림), {"xor": n}(비트 반전), {"error": "parity"}처럼 parity, framing, overrun, break 오류를 고릅니다. {"noise": {"byte": n, "error": ...}}는 잡음 문자 하나를 즉시 전달합니다.

케이블 분리(unplug)도 명령으로 시험합니다. 분리한 노드는 버스에서 송신하지도 수신하지도 않습니다.

type 응용 프로그램이 여는 것 운영체제
pty 의사 터미널(pseudo terminal). /dev/ttys…나 /dev/pts/… 경로이며, link로 고정 경로의 심볼릭 링크를 만듭니다. macOS, Linux
driver 설치한 포트 드라이버의 포트 index Windows(COMn), Linux(/dev/ttyPDVn)
tcp_server listen 주소의 원시 TCP 소켓. 바이트만 오가며 회선 설정은 랩 설정을 따릅니다. 한 번에 클라이언트 하나만 받습니다. 모든 OS
rfc2217_server listen 주소의 RFC 2217 서버. 클라이언트가 속도, 형식, 신호, 브레이크를 원격으로 제어합니다. 모든 OS

RFC 2217은 Telnet COM-PORT-OPTION으로 원격 시리얼 포트를 제어하는 표준입니다(IETF RFC 2217). RFC 2217에는 인증이 없습니다. 그래서 랩은 TCP 서버를 루프백 주소(127.0.0.1)에서만 엽니다. 다른 주소로 공개하려면 랩에 "allow_remote": true를 명시합니다. 신뢰할 수 있는 네트워크에서만 공개합니다.

반대 방향도 있습니다. 랩의 bridges는 로컬 노드를 원격 RFC 2217 장치 서버의 실제 포트로 잇습니다. 응용 프로그램이 바꾼 속도, 형식, 신호, 브레이크가 원격 포트로 전달됩니다. 연결 시간은 원격 UART가 정합니다.

의사 터미널은 운영체제가 UART 없이 제공하는 터미널 장치입니다. 운영체제별로 다음처럼 동작합니다.

  • Linux 의사 터미널은 응용 프로그램이 요청한 데이터 비트와 패리티를 무시합니다. 랩은 노드에 설정한 데이터 비트와 패리티를 사용하고, 속도와 정지 비트만 응용 프로그램 설정을 따릅니다. macOS 의사 터미널은 형식도 보관하므로 모든 값이 응용 프로그램 설정을 따릅니다.
  • Linux의 glibc tcsetattr()는 의사 터미널이 요청한 패리티나 데이터 비트를 버리면 EINVAL(Invalid argument)을 돌려줍니다. 그래서 패리티만 바꾸는 요청(예: 8N1에서 8E1로)은 오류로 끝납니다. pyserial처럼 tcsetattr()로 포트를 설정하는 프로그램은 포트를 열 때 실패할 수 있습니다. 이런 프로그램은 Linux에서 8N1로 열고, 실제 형식은 랩의 노드 line에 설정합니다. 커널의 TCSETS2 ioctl로 설정하는 프로그램(PDComm Modbus SDK 등)은 이 오류를 받지 않습니다.
  • BSD 계열 의사 터미널에서는 mark·space 패리티를 요청할 수 없습니다. 랩의 노드 설정이 그대로 유지됩니다.
  • 랩은 데이터를 처리하기 직전에 각 의사 터미널의 회선 설정을 다시 읽습니다. 그래서 응용 프로그램이 바꾼 설정은 그 뒤에 어느 쪽에서 보내는 데이터에든 적용되고, 포트를 열고 설정한 직후 바로 보내도 됩니다. 데이터 없이 설정만 바꾸면 추적 기록의 port 사건은 약 20ms 안에 남습니다.

랩의 trace에 파일 경로를 지정하면 모든 회선 사건을 JSON Lines로 기록합니다. 한 줄이 사건 하나입니다. t_ns는 랩 시작부터 지난 나노초, node는 노드 이름, event는 사건 종류입니다.

{"t_ns":443484625,"node":"master","event":"tx","byte":1,"on_wire":true}
{"t_ns":444057542,"node":"drive1","event":"rx","byte":1,"error":null}
{"t_ns":842903959,"node":"master","event":"rx","byte":1,"error":"parity"}
{"t_ns":1851553667,"node":"drive2","event":"attached","attached":false}
event 의미
tx 노드가 문자를 보냈습니다. on_wire가 false면 드라이버가 꺼졌거나 케이블이 분리되어 회선에 나가지 않았습니다.
rx 노드가 문자를 받았습니다. error는 null, break, framing, parity, overrun 중 하나입니다.
collision, dropped 버스 충돌, 버려진 문자
signals, inputs, break 모뎀 출력(RTS·DTR), 입력(CTS·DSR·DCD·RI), 브레이크 변화
port 응용 프로그램이 회선 설정을 바꿨습니다.
attached 케이블을 연결하거나 분리했습니다.
tx_empty 송신 버퍼가 비었습니다.

가상 포트와 실제 하드웨어로 시험하는 항목

섹션 제목: “가상 포트와 실제 하드웨어로 시험하는 항목”

가상 포트는 UART 위의 소프트웨어 계층을 시험합니다. 전기 계층은 실제 하드웨어로 시험합니다.

가상 포트로 시험 실제 하드웨어로 시험
속도, 데이터 비트, 패리티, 정지 비트와 설정 불일치 차동 신호, 종단 저항, 바이어스, 접지, 케이블 길이와 잡음 내성
문자 시간 기반 타이밍, 프레임 간격, 16550 수신 FIFO 실제 트랜시버의 방향 전환 지연, USB 변환 칩 고유 지연
RTS/CTS, DTR/DSR/DCD 신호, XON/XOFF, 브레이크 실제 장비와의 수락 시험
RS-485 멀티드롭 주소, 충돌, RTS 방향 제어
시드로 재현하는 결함 주입