이전 글 — Modbus 입문 가이드 2편: CRC-16 생성 원리와 수신 데이터 파싱
2편에서 Modbus RTU 수신 데이터를 WTOB 로 분해하고, CRC-16 생성 원리로 검증하고, BTOW 로 복원하는 파싱 전 과정을 살펴보았습니다.
그런데 한 가지 빠진 것이 있습니다.
그 수신 데이터는 애초에 어떻게 PLC 안으로 들어오는가? 입니다.
이번 3편에서는 미츠비시 Q 시리즈의 시리얼 통신 모듈 QJ71C24N 을 무수순 교신으로 제어해서, Modbus RTU 응답 프레임을 PLC 내부 레지스터까지 받아오는 과정을 다룹니다.
2편의 파싱이 동작하기 위한 바로 앞 단계 — 파라미터 설정, 수신 종료 판정, 데이터 복사, 버퍼 초기화 — 가 이번 편의 주제입니다.
Modbus 의 펑션코드 체계, 레지스터 영역(Coil / Holding / Input Register), 프레임 구조가 아직 생소하다면 Modbus 입문 가이드 1편: 프로토콜 구조 이해 를 먼저 읽고 오시면 이 글이 훨씬 쉽게 읽힙니다.
1. 왜 무수순 교신인가
GX Works2 에는 Modbus RTU 를 지원하는 사전정의 프로토콜(Predefined Protocol) 라이브러리가 있습니다.
디바이스만 지정하면 자동으로 교신하게 꾸밀 수 있어 편리하고, 가장 흔히 쓰는 방법입니다.
그럼에도 이번 편은 그 사전정의 프로토콜을 쓰지 않고, 기존의 시리얼 통신 무수순(무순서) 교신 방식으로 Modbus RTU 를 직접 구현합니다.
이유는 현장에서 사전정의 프로토콜을 쓸 수 없거나, 프레임을 직접 제어해야 하는 상황이 의외로 자주 생기기 때문입니다.
- 상대 기기가 표준과 조금 다른 응답 형식을 사용하는 경우
- 여러 슬레이브를 하나의 시퀀스로 세밀하게 제어해야 하는 경우
- 통신 오류 시 재시도·예외 처리 로직을 직접 설계해야 하는 경우
흔히들 "무수순으로 RTU 는 안 된다"고 알고 계시지만, 당연하게도 안 되는 것은 없습니다.
단지 조금 더 알아야 할 것이 있을 뿐입니다.
그 "알아야 할 것"의 핵심이 바로 다음 두 섹션입니다.
이 글의 래더 예제는 오토닉스 온도컨트롤러 TX · TK 시리즈 기반으로 작성되었습니다. 슬레이브 기기의 통신 매뉴얼을 첨부하니 펑션코드·레지스터 맵을 함께 참고하세요.
2. 무수순 교신이란 무엇인가
무수순 교신은 미츠비시 시리얼 통신에서 사전에 정의된 프로토콜 없이 송수신 버퍼를 직접 제어하는 방식입니다.
전통적으로 무수순 교신은 ASCII 기반에서 시작 문자(STX)와 종료 문자(ETX, 또는 CR+LF)로 프레임의 경계를 구분하는 방식으로 구현합니다.
"이 문자가 오면 프레임 시작, 저 문자가 오면 프레임 끝"이라는 식이죠.
종료 문자가 들어온 순간 "한 덩어리의 메시지가 완성됐다"고 판정하는 구조입니다.
이 방식은 텍스트(ASCII) 기반 장비에서는 매우 직관적이고 안정적입니다.
그런데 Modbus RTU 에 이 방식을 그대로 적용하려고 하면 정확히 이 "종료 문자" 지점에서 막힙니다.
왜 그런지 보겠습니다.
3. RTU 를 무수순으로 구현하기 까다로운 이유
3-1. RTU 와 ASCII 의 결정적 차이
같은 Modbus 라도 바이너리 기반의 Modbus RTU 와 아스키 기반의 Modbus ASCII 는 프레임을 다루는 방식이 다릅니다.
| 구분 | Modbus RTU | Modbus ASCII |
|---|---|---|
| 데이터 기반 | 바이너리 | 아스키코드 |
| 종료 기준 | 무신호 시간 (3.5문자) | 특정 종료문자 (ETX, CR+LF 등) |
| 데이터 검증 | CRC-16 | LRC |
차이를 한눈에 보기 위해, 1번 국 슬레이브의 Holding Register 0000 번지부터 10개를 읽는 동일한 요청을 두 방식으로 표현해 보겠습니다.
요청 의미 : 1번국 Holding Register 0000 부터 10개 읽기 (펑션 03)
공통 구조 : [국번][펑션][시작 어드레스][데이터 갯수]
Modbus RTU : [국][펑션][시작어드][갯수][CRC-16]
(HEX) 0x01 03 0000 000A C5CD
Modbus ASCII : [시작 ":"][국][펑션][시작어드][갯수][LRC][종료 "CR+LF"]
(HEX) 0x3A 3031 3033 30303030 30303041 3731 0D0A
ASCII 는 맨 앞에 시작 문자 : 가, 맨 뒤에 종료 문자 CR+LF 가 분명히 붙습니다.
반면 RTU 는 시작 문자도 종료 문자도 없이 바이너리 값이 곧바로 시작해서 CRC-16 으로 끝납니다.
3-2. 종료 문자가 없다 + CRC 가 매번 바뀐다
여기서 무수순 구현의 진짜 난점이 나옵니다.
ASCII 는 종료 문자(CR+LF)가 있으니 "이 바이트가 오면 프레임 끝"이라고 확정할 수 있습니다.
하지만 RTU 는 종료 문자 없이 CRC-16 으로 문장이 끝납니다.
게다가 그 CRC 값은 응답 데이터에 따라 매번 달라집니다.
응답 길이도 가변이고, 마지막 2바이트인 CRC 값도 고정이 아니라는 뜻입니다.
RTU 는 ASCII 처럼 종료문자(ETX·CR+LF)로 프레임 끝을 찍을 수 없습니다. 게다가 응답 길이가 가변이라 CRC 값도 매번 달라지므로, "특정 바이트가 오면 끝"이라는 종료문자 기반 무수순 로직을 그대로 쓰면 프레임 경계를 영영 못 잡습니다. 끝 판정의 기준을 "문자"가 아니라 "시간(무신호 구간)" 으로 바꿔야 하는 이유입니다.
3-3. RTU 의 진짜 종료 규약: 3.5문자 무신호
그렇다면 RTU 는 프레임의 끝을 어떻게 정하는가? 규약은 단 하나입니다.
"3.5문자 시간 이상 무신호(Idle)가 발생하면 하나의 프레임 종료"
프레임 안에서는 문자들이 촘촘히 이어져 들어오다가, 한 메시지가 끝나면 일정 시간 이상 아무 데이터도 오지 않는 구간이 생깁니다.
그 무신호 구간이 3.5문자를 전송할 수 있는 시간을 넘으면 "프레임이 끝났다"고 보는 것입니다.
즉 RTU 의 종료는 '특정 문자'가 아니라 '통신 간격(시간)'으로 판단합니다.
이 시간은 전송 속도에 따라 달라집니다.
가장 흔한 9600bps 기준으로 직접 계산해 보겠습니다.
[9600bps 기준 3.5문자 무신호 시간 계산]
① 1문자 = 스타트 비트(1) + 데이터(8) + 스탑 비트(1) = 10 비트
② 초당 전송 문자 수 = 9600 ÷ 10 = 960 문자/초
③ 1문자 전송 시간 = 1000ms ÷ 960 ≈ 1.042 ms
④ 3.5문자 시간 = 1.042ms × 3.5 ≈ 3.645 ms
→ 약 3.645ms 이상 무신호가 발생하면 프레임 종료로 판정
추가로, 한 프레임 내부의 문자 간 간격은 1.5문자 이내여야 정상으로 봅니다.
문자 간격이 1.5문자 이상 벌어지면 그 프레임은 불완전(NG)으로 처리하라는 의미입니다.
1.5문자 간격(프레임 내부 바이트 간격) 검사까지 PLC 래더로 엄밀히 구현하기는 부담스럽습니다. 실무에서는 3.5문자 무신호(프레임 종료) 판정만 확실히 잡아도 대부분 안정적으로 동작하므로, 1.5문자 검사는 개념으로 알아두고 우선순위를 낮춰도 됩니다.
여기까지가 "왜 RTU 가 무수순에서 까다로운가" 입니다.
핵심은 끝을 문자가 아니라 시간으로 판단해야 한다는 관점의 전환입니다.
이 관점을 가지고 이제 QJ71C24N 실제 설정으로 들어갑니다.
4. QJ71C24N 파라미터 설정: 종료코드 제거
무수순 RTU 구현에서 가장 먼저 해야 할 일은 수신 종료코드를 반드시 제거하는 것입니다.
앞 섹션의 "RTU 는 종료문자가 없다"는 원칙을 모듈 설정에 그대로 반영하는 단계입니다.
GX Works2 파라미터 → 시리얼 통신 파라미터에서 아래와 같이 설정합니다.
| 버퍼 메모리 | CH1 | CH2 | 설정값 |
|---|---|---|---|
| 수신 종료 코드 | G165 | G325 | 0xFFFF (종료코드 없음) |
| 수신 종료 길이 | G164 | G324 | 0x1FF (기본값 유지) |

종료코드를 제거해야 하는 이유는 Modbus RTU 가 바이너리 통신이기 때문입니다.
아스키 통신에서 숫자 3은 0x33 이지만, RTU 바이너리 통신에서는 0x03 입니다.
만약 종료코드를 ETX(0x03) 으로 설정하면, 슬레이브로부터 데이터 값 3 이 들어오는 순간 모듈이 이를 종료코드로 오인하고 프레임 수신을 중단해버립니다.
데이터가 중간에서 잘리는 것이죠.
0x03 만의 문제가 아닙니다.
아스키코드 표에 있는 어떤 값을 종료코드로 설정해도 동일한 문제가 발생합니다.
RTU 프레임 안에는 어떤 바이트 값이든 정상 데이터로 나타날 수 있기 때문입니다.
RTU 는 바이너리 통신이라 데이터 중간에 어떤 바이트값이든 정상 데이터로 들어올 수 있습니다. 종료코드를 설정해두면 데이터 중간의 우연한 일치 바이트를 프레임 끝으로 오인해 교신이 꼬입니다. 반드시 수신 종료코드(G165 / G325)를 0xFFFF(미사용) 로 두세요. 이 한 가지를 놓치면 통신이 간헐적으로만 성공하는, 재현 어려운 버그가 됩니다.
5. 수신 버퍼 메모리 구조
QJ71C24N 은 수신된 데이터를 내부 버퍼 메모리에 저장합니다. 이 버퍼를 직접 감시하고 읽어오는 것이 무수순 RTU 구현의 핵심입니다.
| 버퍼 메모리 | CH1 | CH2 | 내용 |
|---|---|---|---|
| 수신 데이터 바이트 수 | G1536 | G2560 | 수신된 총 바이트 수 |
| 수신 데이터 저장 영역 | G1537~ | G2561~ | 수신된 원시 데이터 |
| 수신 데이터 초기화 요구 | G168 | G328 | K1 SET → 모듈 내부 링 버퍼 클리어 |
6. 수신 종료 판정: 무신호 구간을 스캔으로 흉내내기
§3 에서 본 RTU 의 종료 규약 "3.5문자 무신호" 를, 종료코드 없이 어떻게 PLC 가 판단할까요?수신 완료(X03/X0A) 신호로는 판단할 수 없습니다.
종료코드를 제거했기 때문입니다.
대신 수신 바이트 수 변화 감시 방식으로 프레임 종료를 판단합니다.
① G1536(수신 바이트 수) > 0 → 수신 진행 중
② 현재 스캔의 G1536 값을 임시 레지스터에 저장 (예: D6257)
③ 다음 스캔에서 G1536 == D6257 → 변화 없음 → 수신 완료 판정
슬레이브가 응답을 보내는 도중에는 G1536 값이 계속 증가합니다.
프레임 전송이 끝나면 3.5문자 무신호 구간이 발생하고, 그 사이 G1536 이 더 이상 변하지 않습니다.
이 "변화 없음" 순간을 1스캔으로 감지하는 것이 핵심입니다.
RTU 가 시간(무신호)으로 끝을 정하듯, PLC 는 그 무신호를 "스캔 간 바이트 수 불변"으로 모방하는 것이죠.
현재 스캔 G1536 = 이전 스캔 저장값(D6257)?
→ YES → 수신 완료
→ NO → D6257 ← G1536 저장 후 다음 스캔 대기
1스캔만으로 불안정하다면 스캔 카운터를 추가해 2~3스캔 더 감시하는 것도 좋습니다.

7. BMOV 로 수신 데이터 복사하기
수신 완료 판정 후 G1537~ 영역의 원시 데이터를 내부 작업 레지스터로 복사합니다.
수신 데이터는 바이트 단위로 카운트되지만 BMOV 는 워드 단위로 동작합니다.
때문에 바이트 수를 워드 수로 변환하는 계산이 필요합니다.
바이트 → 워드 변환 계산
워드 수 = (수신 바이트 수 ÷ 2) 의 몫 + 나머지
예1) 수신 10바이트 → 10 ÷ 2 = 몫 5, 나머지 0 → 5 워드
예2) 수신 5바이트 → 5 ÷ 2 = 몫 2, 나머지 1 → 3 워드
나머지가 1이더라도 워드 수에 더해줘야 마지막 바이트가 잘리지 않습니다.
래더 처리 순서
U□\G1536값을 내부 레지스터로 읽기 (□자리에는 QJ71C24N 이 장착된 슬롯 번호. 예: 슬롯 0 →U0\G1536)DIV명령으로 K2 나누기 → 몫·나머지 임시 저장ADD명령으로 몫 + 나머지 = BMOV 워드 수 확정BMOV U□\G1537 D_dst K_n(확정된 워드 수만큼)
버퍼메모리 표기 — Q-series 표준은 U□\G어드레스 직접 접근입니다. FROM 명령(FROM H_ K1537 D_dst K_n) 으로도 동일 동작이 가능하나, BMOV + U□\G 가 가독성과 스캔타임 모두 우수합니다. 슬롯 변경 시 U□ 부분만 수정하면 이식이 끝납니다.

이 BMOV 가 완료되면 슬레이브로부터 수신된 Modbus RTU 응답 프레임 전체가 PLC 내부 레지스터에 담기게 됩니다.
그 다음 단계인 WTOB 분해 → CRC-16 검증 → BTOW 복원 파싱은 2편 에서 다뤘습니다.
8. 수신 버퍼 초기화
데이터 복사가 끝났다고 바로 다음 요청을 보내면 안 됩니다.
초기화를 먼저 해야 합니다.
초기화 없이 다음 요청을 보내면 이전 응답 데이터가 버퍼에 잔류합니다.
다음 응답이 들어왔을 때 이전 잔류 데이터와 뒤섞여 파싱 오류가 발생합니다.
초기화 순서
MOV K1 U□\G168(CH1) 또는MOV K1 U□\G328(CH2)
→ 모듈 내부 링 버퍼 클리어 요청.
모듈이 클리어 완료 후 해당 플래그를 자동으로 RST 합니다.TO H_ K168 K1 K1 (구식 표기) 으로도 가능하나, MOV K1 U□\G168 직접 어드레싱이 권장됩니다.
- 내부 작업 레지스터(수신 바이트 수 미러, BMOV 복사 영역)를
FMOV K0으로 0 클리어

수신 버퍼 클리어를 빠뜨리면 이전 응답의 잔여 바이트가 다음 프레임 앞에 붙어 국번·CRC 검증이 어긋납니다. "수신 처리 완료 → 버퍼 초기화 → 다음 요청 송신" 순서는 선택이 아니라 필수입니다.
9. 전체 수신 처리 흐름
지금까지 설명한 흐름을 한눈에 정리하면 이렇습니다.
송신 요청 프레임 전송
↓
G1536 > 0 감시 (수신 시작 대기)
↓
G1536 변화 멈춤 감지 (1~3스캔) → 수신 완료 판정 ← §6, "3.5T 무신호"의 스캔 모방
↓
바이트 수 → 워드 수 변환 (DIV + ADD)
↓
BMOV 로 G1537~ 데이터 내부 레지스터로 복사
↓
WTOB 분해 → CRC-16 검증 → BTOW 복원 (2편)
↓
G168(CH1) / G328(CH2) SET → 수신 버퍼 클리어
↓
다음 요청 대기
마치며
Modbus RTU 는 종료문자로 끝나는 통신이 아니라 타이밍(3.5문자 무신호)과 CRC 로 완성되는 프레임 통신입니다.
그래서 GX Works2 에서 무수순 교신으로 RTU 를 구현하려면, "종료코드를 찾는 로직"이 아니라 "데이터 흐름이 멈췄는지를 판단하는 구조" 를 설계해야 합니다.
반드시 기억할 두 가지는 이것입니다.
- 수신 종료코드(G165 / G325)를 0xFFFF 로 제거할 것
- CRC 검증과 프레임 파싱을 끝낸 뒤 수신 버퍼메모리를 반드시 클리어할 것
이 관점을 가지면 RTU 통신은 더 이상 까다롭지 않습니다.
이 글에서 다룬 수신 프레임의 CRC-16 이 어떻게 만들어지는가(자리올림 없는 나눗셈 = XOR·시프트 생성 원리)는 Modbus 입문 가이드 2편: CRC-16 생성 원리와 수신 데이터 파싱 에서 손계산까지 정리했습니다. 이를 Q-series 래더(우시프트 SFR·캐리 플래그 SM700 을 이용한 bit0 분기, 두 겹 FOR~NEXT 루프)로 구현하고 XCALL 함수로 재사용 가능하게 라이브러리화한 버전은 함수화 6편: CRC-16/MODBUS 코드 생성 에서 함께 참고하세요.
실제 래더 예제
아래는 본문에서 설명한 무수순 RTU 수신 처리와 CRC-16 검증의 실제 래더 예제입니다.


전체 프로젝트는 GX Works2 파일로 첨부합니다.
오토닉스 TX·TK 온도컨트롤러 기반 예제이며, 본문의 파라미터 설정·수신 처리·CRC 검증 로직이 모두 포함되어 있습니다.
'PLC 전기제어 기술자료 > 통신 기술자료' 카테고리의 다른 글
| Modbus 입문 가이드 2편: CRC-16 생성 원리와 수신 데이터 파싱 (0) | 2026.05.24 |
|---|---|
| Modbus 입문 가이드 1편: 프로토콜 구조 이해 (RTU · TCP · 펑션코드) (0) | 2026.05.22 |
| [미립자팁] QJ71E71-100 이더넷 통신모듈 COM.ERR 리셋 (0) | 2026.01.28 |
| [MELSEC] (미립자팁) CC-LINK IE CONTROL 통신으로 인해 스캔타임이 느릴때 조치할만한 방법 (고정주기스캔설정) (0) | 2022.05.14 |