산업 자동화 현장에서 일하다 보면 Modbus 라는 이름을 끊임없이 마주칩니다.
인버터, 온도 컨트롤러, 유량계, 압력 트랜스미터, 스마트 센서 — 거의 모든 장비의 통신 사양서 첫 페이지에는 Modbus RTU 또는 Modbus TCP 가 적혀 있죠.
그런데 막상 구현을 시작하려고 하면 이런 의문들이 생깁니다.
RTU 와 TCP 는 뭐가 다른가?
FC03 과 FC04 는 왜 구분되어 있는가?
CRC-16 은 어떻게 계산하는가?
MBAP 헤더가 뭔가?
이 글에서는 Modbus 프로토콜의 구조부터 RTU 와 TCP 의 차이, 펑션코드 체계, 그리고 프레임 구성까지 한 번에 정리합니다.
1. Modbus 란 무엇인가
Modbus 는 1979년 Modicon (현재 Schneider Electric) 이 자사 PLC 간 통신을 위해 개발한 마스터-슬레이브 직렬 통신 프로토콜입니다.
공개 표준으로 전환된 이후 산업 자동화 분야의 사실상 표준(de facto standard) 으로 자리 잡았습니다.
지금도 수십 년 전 설계된 프로토콜이 현역으로 사용되는 이유는 단 하나입니다.
구조가 단순하기 때문입니다.
요청 한 번에 응답 한 번. 복잡한 핸드셰이크도 없고, 라이선스도 없습니다. 임베디드 환경에서 구현하기 쉽고, 디버깅도 쉽습니다.
Modbus 파생 표준
Modbus 는 전송 계층에 따라 세 가지 파생 표준으로 나뉩니다.
| 표준 | 전송 계층 | 특징 |
|---|---|---|
| Modbus RTU | RS-232 / RS-485 | 바이너리 프레임, CRC-16, 무신호 구간으로 프레임 경계 구분 |
| Modbus ASCII | RS-232 / RS-485 | ASCII 인코딩, LRC 체크섬, : 시작 + CR LF 종료 |
| Modbus TCP | Ethernet (TCP/IP) | MBAP 헤더 추가, CRC 없음, 다중 연결 가능 |
산업 현장에서는 RTU 와 TCP 가 주력입니다. ASCII 는 구형 장비 호환 목적으로만 간헐적으로 등장합니다.
Modbus 메시지의 핵심은 PDU(Protocol Data Unit) 입니다. 어떤 전송 계층이든 "펑션코드 + 데이터" 라는 PDU 구조는 동일합니다.

시리얼 라인(RTU·ASCII)에서는 이 PDU 앞뒤로 슬레이브 주소와 오류 검출(CRC-16 / LRC) 이 감싸면서 하나의 완성된 프레임이 됩니다.

2. 마스터-슬레이브 구조
Modbus 는 철저한 마스터-슬레이브 구조로 동작합니다.
- 마스터: 통신을 먼저 시작하는 쪽. PLC, HMI, SCADA 등
- 슬레이브: 요청을 받아 응답하는 쪽. 인버터, 센서, 계측기 등
마스터 ─── 요청(Request) ───→ 슬레이브
마스터 ←── 응답(Response) ─── 슬레이브
슬레이브는 절대로 먼저 말을 걸지 않습니다. 마스터의 요청이 있어야만 응답합니다.
RTU 에서는 버스 1개에 마스터 1대, 슬레이브 최대 247대까지 연결할 수 있습니다.
TCP 에서는 IP 기반으로 이론상 제한 없이 연결 가능합니다.
3. 디바이스(레지스터) 영역 구분
Modbus 슬레이브 내부 데이터는 네 가지 영역으로 구분됩니다.
| 영역명 | 주소 표기 | 범위 | 접근 | 용도 |
|---|---|---|---|---|
| Coil | 0xxxx | 00001~09999 | R/W | 디지털 출력. 마스터가 쓰고 읽음 |
| Discrete Input | 1xxxx | 10001~19999 | R | 디지털 입력. 슬레이브 하드웨어 전용 |
| Input Register | 3xxxx | 30001~39999 | R | 아날로그 입력·측정값. 슬레이브 전용 |
| Holding Register | 4xxxx | 40001~49999 | R/W | 설정값·제어 파라미터. 범용 |
비트 단위 데이터는 Coil / Discrete Input 영역을, 16비트 워드 데이터는 Input Register / Holding Register 영역을 사용합니다.
표의 R/W · R 은 슬레이브 내부 메모리 속성입니다. 마스터는 항상 요청을 보내는 쪽이며, 슬레이브 메모리가 R/W 이면 마스터가 읽고 쓸 수 있다는 의미입니다.
한 가지 중요한 점이 있습니다.
프레임 내 실제 주소는 표기 주소 - 1 입니다 (0-based).
Holding Register 40001번지를 읽으려면 프레임에는 0x0000 을 넣어야 합니다.
40100번지는 0x0063 입니다.
단, 매뉴얼마다 표기 방식이 다릅니다. 40001 처럼 5자리 형식이면 1-based 이므로 1을 빼서 사용하세요. 0x0000 또는 0 으로 시작하는 형식이면 0-based 이므로 그대로 사용하면 됩니다.
장비 매뉴얼을 받으면 데이터 영역(Coil / Register 종류)과 주소 체계(1-based / 0-based)를 가장 먼저 확인하는 습관을 들이세요. 이 두 가지만 정확히 맞춰도 통신이 안 될 때의 디버깅 시간을 크게 줄일 수 있습니다.
4. 펑션 코드 (Function Code)
펑션 코드는 무엇을 어떻게 읽고 쓸지 를 지정하는 명령어입니다.
| FC | 이름 | 동작 | 대상 영역 | 단위 |
|---|---|---|---|---|
| 01 | Read Coils | 읽기 | Coil (0xxxx) | 비트 |
| 02 | Read Discrete Inputs | 읽기 (전용) | Discrete Input (1xxxx) | 비트 |
| 03 | Read Holding Registers | 읽기 | Holding Register (4xxxx) | 16bit 워드 |
| 04 | Read Input Registers | 읽기 (전용) | Input Register (3xxxx) | 16bit 워드 |
| 05 | Write Single Coil | 1비트 쓰기 | Coil (0xxxx) | 비트 |
| 06 | Write Single Register | 1워드 쓰기 | Holding Register (4xxxx) | 16bit 워드 |
| 0F (15) | Write Multiple Coils | 다비트 쓰기 | Coil (0xxxx) | 비트 |
| 10 (16) | Write Multiple Registers | 다워드 쓰기 | Holding Register (4xxxx) | 16bit 워드 |
현장에서 가장 많이 사용하는 조합은 FC03 (Holding Register 읽기) 와 FC10 (Holding Register 다중 쓰기) 입니다.
FC03 과 FC04 — 둘 다 읽기인데 왜 구분되나?
처음 Modbus 를 공부할 때 가장 많이 혼동하는 부분입니다.
| 항목 | FC03 | FC04 |
|---|---|---|
| 대상 영역 | Holding Register (4xxxx) | Input Register (3xxxx) |
| 접근 권한 | R/W | R (읽기 전용) |
| 용도 | 설정값·제어 파라미터 | 측정값·센서 데이터 |
| 예시 | 인버터 주파수 설정값, PID 목표값 | 현재 전류값, 온도 센서 측정값 |
FC03 과 FC04 는 완전히 별개의 주소 공간입니다.
슬레이브 내부에서 40001번지와 30001번지는 다른 메모리 영역입니다. 같은 숫자처럼 보여도 전혀 다른 데이터가 들어 있습니다.
장비 매뉴얼에 "Input Register" 로 표기된 주소는 반드시 FC04 로 읽어야 합니다. FC03 으로 읽으면 같은 번지여도 전혀 다른 영역의 데이터가 나옵니다. 통신은 정상인데 값이 이상할 때 가장 먼저 의심할 부분입니다.
실제 장비 매뉴얼은 펑션코드별 메시지 형식을 표로 제공합니다. 아래는 오토닉스 온도컨트롤러 매뉴얼의 데이터 읽기 펑션(FC04) 예시로, [국번][펑션코드][시작번지][데이터 갯수][CRC-16] 형식과 바이너리 송수신 예까지 보여줍니다.

5. Modbus RTU 프레임 구조
RTU 의 기본 프레임 구조는 단순합니다.
[ Slave Addr ][ Function Code ][ Data (N bytes) ][ CRC-Lo ][ CRC-Hi ]
1 byte 1 byte 1 byte 1 byte
FC03 요청 프레임 예시 — 슬레이브 01번, 0x006B번지부터 3워드 읽기
01 03 00 6B 00 03 76 87
│ │ └─────┘ └─────┘ └────┘
│ │ 시작주소 읽기수 CRC-16
│ FC03
슬레이브01
FC03 응답 프레임 예시
01 03 06 02 2B 00 00 00 64 B9 12
│ │ │ └────────────────┘ └─────┘
│ │ │ 데이터 (3워드 = 6바이트) CRC-16
│ │ Byte Count (6)
│ FC03
슬레이브01
Byte Count = 요청 워드 수 × 2 입니다. 3워드를 읽었으니 6바이트.
RTU 메시지 프레임을 도식으로 보면, 슬레이브 주소와 펑션코드 뒤에 데이터가 오고 맨 끝에 CRC-16 이 붙는 구조가 한눈에 들어옵니다.

RTU 의 프레임 경계 — 무신호 구간
RTU 에서 프레임의 시작과 끝은 특정 문자가 아니라 무신호 구간으로 구분합니다.
─── 3.5T ───│ 01 03 00 6B 00 03 76 87 │─── 3.5T ───│ 다음 프레임 │
└────── 요청 프레임 ──────┘
보레이트 기준 1바이트 전송 시간을 1 character time (1T) 이라 할 때:
- 프레임과 프레임 사이: 3.5T 이상 무신호 → 프레임 경계
- 같은 프레임 내 바이트 간격: 1.5T 이하 유지 (초과 시 프레임 폐기)
9600bps 기준으로 계산하면:
- 1T = 1000ms ÷ (9600 ÷ 10) = 약 1.042ms
- 3.5T = 약 3.645ms 이상 무신호 → 프레임 종료
프레임과 프레임 사이의 무신호 구간이 종료 기준이 되는 모습을 도식으로 보면 다음과 같습니다.

3.5T·1.5T 시간을 보레이트별로 계산한 표는 다음과 같습니다.

6. CRC-16 오류 검출
RTU 프레임 끝에는 반드시 CRC-16 이 2바이트 붙습니다. Low Byte 먼저, High Byte 나중 순서입니다.
CRC-16 의 파라미터는 다음과 같습니다.
| 항목 | 값 |
|---|---|
| 초기값 | 0xFFFF |
| 다항식 | 0xA001 |
계산 절차를 의사코드로 표현하면 이렇습니다.
CRC = 0xFFFF
for each byte B in (슬레이브주소 ... 데이터 마지막 바이트):
CRC = CRC XOR B ← 하위 바이트에 현재 바이트 XOR
for i = 0 to 7:
if (CRC AND 0x0001) != 0: ← LSB 검사
CRC = (CRC SHR 1) XOR 0xA001
else:
CRC = CRC SHR 1
return CRC ← Low Byte 먼저 프레임 끝에 추가
위 의사코드의 SHR (우시프트) 와 AND 0x0001 (bit0 검사) 두 가지는 Mitsubishi Q-series 에 그대로 존재하지 않습니다. 대신 우시프트 1비트 명령(SFR)이 시프트와 동시에 밀려난 bit0 을 시스템 캐리 비트(SM700)에 자동으로 저장하므로, 이 캐리 비트를 읽어서 분기에 사용합니다.
CRC 계산 결과를 수신 프레임의 CRC 와 비교한 다음, 일치 여부에 따라 데이터 처리 또는 재요청으로 갈립니다.
[수신 완료]──[CRC 일치]────────────────( 데이터 처리 OK )
[수신 완료]──[CRC 불일치]───────────────( 재요청 요구 )
구현 디테일(SFR·SM700 캐리 동작, 두 겹 루프 구조, 실제 적용 라이브러리)은 2편 — CRC-16 래더 구현과 수신 데이터 파싱 에서 상세히 다룹니다.
슬레이브로부터 응답이 돌아오면 동일한 방법으로 CRC 를 재계산해서 프레임 마지막 2바이트와 비교합니다. 불일치 시 수신 에러로 처리하고 재시도합니다.
예외 응답 (Exception Response)
슬레이브가 요청을 처리할 수 없을 때는 요청 FC | 0x80 값으로 응답합니다.
예를 들어 FC03 요청에 에러가 발생하면 응답 FC = 0x83 이 됩니다.
| Exception Code | 의미 |
|---|---|
| 01 | 지원하지 않는 펑션 코드 |
| 02 | 주소 범위 초과 |
| 03 | 데이터 값 오류 |
| 04 | 슬레이브 내부 오류 |
| 06 | 슬레이브 처리 중 — 재시도 필요 |
7. RTU vs TCP — 무엇이 다른가
RTU 와 TCP 는 같은 Modbus 명령 체계를 쓰지만 전송 계층이 완전히 다릅니다.
| 항목 | Modbus RTU | Modbus TCP |
|---|---|---|
| 물리 계층 | RS-232 / RS-485 | Ethernet |
| 포트 | — | 502 (IANA 표준) |
| 프레임 헤더 | 슬레이브 주소 (1바이트) | MBAP Header (6바이트) |
| 프레임 경계 | 3.5T 무신호 구간 | TCP 스트림 — Length 필드로 결정 |
| 오류 검출 | CRC-16 (2바이트, 프레임 끝) | TCP 자체 오류 검출 — CRC 없음 |
| 트랜잭션 ID | 없음 | MBAP 에 포함 (요청·응답 매칭용) |
| 멀티마스터 | 불가 (버스 공유, 마스터 1대) | 가능 (TCP 연결 다중화) |
| 최대 슬레이브 | RS-485 기준 최대 247대 | IP 기반 — 이론상 제한 없음 |
| 연결 설정 | 없음 (버스 직결) | TCP 3-way handshake 필요 |
| 재접속 처리 | 해당 없음 | 접속 끊김 시 재접속 시퀀스 필요 |
RTU 는 배선이 단순하고 장거리 RS-485 버스로 많은 장비를 묶을 수 있습니다.
TCP 는 기존 Ethernet 인프라를 그대로 활용하고, 다중 마스터 접속과 빠른 속도가 장점입니다.
8. Modbus TCP 프레임 구조 — MBAP 헤더
TCP 에서는 RTU 프레임 앞에 MBAP (Modbus Application Protocol) 헤더 6바이트가 추가됩니다.
[ Transaction ID ][ Protocol ID ][ Length ][ Unit ID ][ FC ][ Data ]
2 byte 2 byte 2 byte 1 byte 1 byte N byte
|<------------- MBAP Header (6 byte) ---------->|<----- PDU ------->|
| 필드 | 크기 | 내용 |
|---|---|---|
| Transaction ID | 2바이트 | 요청·응답 매칭 식별자. 마스터가 설정, 슬레이브가 그대로 반환 |
| Protocol ID | 2바이트 | 항상 0x0000 (Modbus 식별자 고정) |
| Length | 2바이트 | Unit ID 이후 바이트 수 |
| Unit ID | 1바이트 | RTU 의 슬레이브 주소와 동일 역할 |
RTU 에 있던 CRC-16 은 TCP 에서 완전히 사라집니다. TCP 계층에서 오류 검출을 처리하기 때문입니다.
FC03 요청 프레임 예시 — TCP, 슬레이브 01번, 0x006B번지부터 3워드 읽기
00 01 00 00 00 06 01 03 00 6B 00 03
TXID PROTO LEN UID FC ADDR QTY
Length = 0x0006 = Unit ID(1) + FC(1) + Start Addr(2) + Qty(2)
Transaction ID 의 역할
RTU 는 요청 1개에 응답 1개가 순서대로 오기 때문에 매칭이 자동으로 됩니다.
TCP 는 여러 요청을 동시에 보낼 수 있고, 응답 순서가 달라질 수 있습니다. Transaction ID 를 통해 어떤 요청에 대한 응답인지 매칭합니다.
요청마다 Transaction ID 를 1씩 증가시키고, 응답에서 동일한 ID 가 돌아오는지 확인하는 방식으로 구현합니다.
9. 레지스터 주소 체계 정리
현장에서 장비 매뉴얼을 보면 주소 표기 방식이 제각각이라 헷갈리는 경우가 많습니다.
핵심 규칙은 하나입니다.
5자리 표기(40001~) 매뉴얼: 표기 주소 - 1 = 프레임 주소
0x0000 표기 매뉴얼: 그대로 사용
| 매뉴얼 표기 | FC | 프레임 주소 |
|---|---|---|
| 40001 | FC03 | 0x0000 |
| 40010 | FC03 | 0x0009 |
| 40100 | FC03 | 0x0063 |
| 30001 | FC04 | 0x0000 |
| 00001 | FC01 | 0x0000 |
또 한 가지 주의할 점은 FC별 1회 최대 읽기 수입니다.
| FC | 1회 최대 |
|---|---|
| FC01 / FC02 (비트) | 2000 비트 |
| FC03 / FC04 (워드) | 125 워드 |
| FC10 (다중 쓰기) | 123 워드 |
FC03 으로 한 번에 125워드 이상을 요청하면 슬레이브가 Exception Code 02 (주소 범위 초과) 로 응답합니다. 데이터가 많다면 나눠서 여러 번 요청해야 합니다.
응답 프레임 크기는 항상 수신 버퍼 크기 안에 들어와야 합니다. 한 번에 읽는 워드 수가 많으면 Byte Count(데이터 바이트 수)가 커져 버퍼를 넘칠 수 있으므로, FC별 최대치(워드 125 / 비트 2000)와 실제 버퍼 크기 중 더 작은 값을 기준으로 분할하세요.
마무리
Modbus 는 오래된 프로토콜이지만, 단순함이 곧 경쟁력입니다.
- RTU : RS-485 버스 기반, CRC-16 오류 검출, 무신호 구간으로 프레임 경계
- TCP : Ethernet 기반, MBAP 헤더, CRC 없음, Transaction ID 로 요청·응답 매칭
- FC03 vs FC04 : 완전히 별개 주소 공간 — 매뉴얼의 Register 종류를 반드시 확인
- 주소 : 표기 주소 - 1 = 프레임 주소 (0-based)
이어지는 2편 — CRC-16 래더 구현과 수신 데이터 파싱 에서는 여기서 정리한 프레임 구조를 바탕으로, 미츠비시 Q 시리즈 PLC 에서 수신 데이터를 실제로 가공하는 과정(WTOB 분해 → 국번·FC 검증 → CRC-16 검증 → BTOW 복원)을 단계별로 다룹니다.
사전정의 프로토콜(Predefined Protocol) 대신 송수신 버퍼를 직접 제어하는 무수순 방식으로 Modbus RTU 를 구현하는 전체 과정은 Q Modbus RTU 무수순 통신 구현 가이드 에서 별도로 정리했습니다. 프레임 구조를 이해했다면 다음 단계로 읽어보세요.
'PLC 전기제어 기술자료 > 통신 기술자료' 카테고리의 다른 글
| Modbus 입문 가이드 3편 : QJ71C24N 무수순 통신으로 RTU 구현하기 (설정·수신편) (0) | 2026.05.31 |
|---|---|
| Modbus 입문 가이드 2편: CRC-16 래더 구현과 수신 데이터 파싱 (0) | 2026.05.24 |
| [미립자팁] QJ71E71-100 이더넷 통신모듈 COM.ERR 리셋 (0) | 2026.01.28 |
| [MELSEC] (미립자팁) CC-LINK IE CONTROL 통신으로 인해 스캔타임이 느릴때 조치할만한 방법 (고정주기스캔설정) (0) | 2022.05.14 |