본문 바로가기
PLC 전기제어 기술자료/통신 기술자료

Modbus 입문 가이드 2편: CRC-16 래더 구현과 수신 데이터 파싱

by 위치결정JP 2026. 5. 24.
반응형

이전 글 — Modbus 입문 가이드 1편: 프로토콜 구조 이해

1편에서 Modbus RTU 프로토콜의 구조 — 펑션코드, 디바이스 영역, RTU/TCP 프레임 형식까지 살펴보았습니다.

이제 버퍼에 담긴 원시 데이터를 실제로 사용할 수 있는 형태로 가공하는 단계입니다.

이번 편에서는 세 가지를 다룹니다.

 

  1. WTOB — 워드 데이터를 바이트 단위로 분해하기
  2. CRC-16 래더 구현 — FOR~NEXT 루프로 검증하기
  3. BTOW + 데이터 추출 — 원하는 값을 워드로 복원하기

 

1. WTOB: 워드를 바이트로 분해하기

QJ71C24N 수신 버퍼는 워드 단위로 데이터를 저장합니다.

Modbus RTU 프레임은 바이트 단위 구조입니다.

이 두 가지가 맞지 않기 때문에 워드로 저장된 수신 데이터를 바이트 단위로 풀어줘야 합니다. 이것이 WTOB 명령의 역할입니다.

 

WTOB  s  d  n
  s : 소스 워드 데이터 선두
  d : 목적지 바이트 배열 선두
  n : 변환할 바이트 수

 

쉽게 풀어쓰면 시작 워드 위치(s), 결과를 저장할 바이트 배열 위치(d), 분해할 바이트 수(n) 세 가지를 지정하면 워드 1개당 상위·하위 바이트 2개로 풀어줍니다.

 

수신 워드 0x0103  →  WTOB  →  0x01, 0x03 (바이트 배열)
수신 워드 0x000A  →  WTOB  →  0x00, 0x0A

 

WTOB 를 적용하면 수신 데이터가 Modbus RTU 프레임의 바이트 위치에 1:1 대응됩니다.

WTOB 호출 조건: 래더 표현

WTOB 를 무조건 매 스캔 실행하면 안 됩니다. 수신이 완료되고 복사가 끝난 시점에만 한 번 실행되도록 조건을 잡아야 합니다.

 

 [수신 완료]──[BMOV 복사 완료]────────────────( WTOB 바이트 분해 실행 )

 

  • 두 조건이 모두 만족될 때만 WTOB 가 실행되어 수신 워드 배열을 바이트 배열로 풀어줍니다.
  • 이후 단계(국번·FC 검증 → CRC 검증 → BTOW)도 같은 방식으로 직전 단계 완료 신호를 조건으로 받아 순차 실행합니다.

 

FC03 응답 프레임 바이트 맵

슬레이브 01번에 FC03 으로 10워드를 읽었을 때 응답 프레임 바이트 배열은 이렇습니다.

 

바이트 위치 내용 레지스터 예시
0 슬레이브 국번 ZR6200
1 펑션코드 ZR6201
2 수신 데이터 바이트 수 (Byte Count) ZR6202
3 데이터 1번 High Byte ZR6203
4 데이터 1번 Low Byte ZR6204
5~ 데이터 2번~ ZR6205~
마지막 -1 CRC-16 Low Byte
마지막 CRC-16 High Byte

 

WTOB 후 가장 먼저 할 일은 국번(ZR6200)과 펑션코드(ZR6201) 검증입니다.

내가 요청한 슬레이브 번호와 FC 가 응답과 일치하는지 확인합니다. 불일치하면 버리고 재요청하는 처리를 추가하면 통신 신뢰도가 높아집니다.

예외 응답 확인

펑션코드 바이트의 MSB(최상위 비트) 가 1이면 슬레이브가 Exception 응답을 보낸 것입니다.

 

FC03 정상 응답  →  ZR6201 = 0x03
FC03 예외 응답  →  ZR6201 = 0x83 (0x03 | 0x80)

 

예외 응답 시 그 다음 바이트(ZR6202)가 Exception Code 입니다.

 

Code 의미
01 지원하지 않는 펑션코드
02 요청 주소 범위 초과
03 요청 데이터 값 오류
04 슬레이브 내부 오류
06 슬레이브 처리 중 — 잠시 후 재시도

 

2. CRC-16 래더 구현

국번과 FC 를 확인했다면 다음은 CRC-16 검증입니다.

수신 프레임이 전송 도중 손상되지 않았는지 확인하는 단계입니다.

CRC-16 알고리즘

파라미터는 두 가지입니다.

 

항목
초기값 0xFFFF
다항식 0xA001

 

0xA001 은 Modbus 표준 다항식 0x8005 를 LSB-first 처리용으로 뒤집은(reflected) 형태입니다.

계산 대상은 프레임의 첫 바이트(국번)부터 CRC 직전 바이트까지입니다. Byte Count 와 데이터 바이트 모두 포함하며, CRC 자체는 계산에 포함하지 않습니다.

계산 절차를 의사코드로 표현하면 다음과 같습니다.

 

① CRC 레지스터 = 0xFFFF 로 초기화

② 국번 바이트부터 CRC 직전 바이트까지 순서대로:
   a. 현재 바이트를 CRC 레지스터 하위 바이트 (Low Byte) 와 XOR
   b. 아래 연산을 8회 반복:
      - CRC bit0 = 1  →  CRC를 오른쪽 1비트 시프트(SHR) 후 0xA001 과 XOR
      - CRC bit0 = 0  →  CRC를 오른쪽 1비트 시프트(SHR) 만 실행

③ 모든 바이트 처리 완료 후 CRC 레지스터 최종값 = 계산된 CRC-16

④ 수신 프레임 마지막 2바이트 (Low Byte 먼저, High Byte 나중) 와 비교
   → 일치  : 정상 프레임
   → 불일치 : 수신 에러 → 버퍼 클리어 후 재요청

 

각 단계를 풀어쓰면 다음과 같습니다.

1단계 — 초기화 CRC 레지스터(16비트)를 0xFFFF 로 채워서 시작합니다.

2단계 — 바이트별 누적 처리 국번 바이트부터 CRC 직전 바이트까지 한 바이트씩 순서대로 처리합니다. 각 바이트마다 다음을 수행합니다.

 

  • 현재 바이트를 CRC 레지스터의 하위 바이트(Low Byte)와 XOR 합니다.
  • 그 다음 비트 시프트를 8회 반복합니다.
  • 시프트 직전 CRC 의 최하위 비트(bit0) 를 먼저 확인합니다.
  • CRC 를 오른쪽으로 1비트 시프트합니다.
  • 시프트 전 bit0 이 1 이었으면 시프트 결과에 다항식 0xA001 을 XOR 합니다.
  • bit0 이 0 이었으면 시프트만 하고 끝납니다.

 

3단계 — 최종 CRC 산출 모든 바이트를 처리하고 나면 CRC 레지스터에 남은 값이 계산된 CRC-16 입니다.

4단계 — 수신값과 비교 계산한 CRC 를 수신 프레임 마지막 2바이트(Low Byte 먼저, High Byte 나중)와 비교합니다.

 

  • 일치: 정상 프레임 → 데이터 추출 단계로 진행
  • 불일치: 수신 에러 → 버퍼 클리어 후 재요청

 

래더 구현 구조

래더에서는 FOR~NEXT 루프를 두 겹으로 사용합니다.

 

⚠️ 주의

스캔타임 주의 — 응답 데이터가 많을수록 루프 횟수가 늘어납니다. 예를 들어 응답 25바이트 기준으로 외부 23회 × 내부 8회 = 184회 반복입니다. 읽는 워드 수가 수십 개 이상이라면 스캔타임 영향을 확인하고, 필요하면 처리 바이트 수를 제한해 여러 스캔에 분산하는 것도 고려하세요.

 

Q-series 명령어 매핑 (의사코드 → 실제 IL)

일반 CRC-16 의사코드에 등장하는 SHR (우시프트) 와 AND 0x0001 (bit0 검사) 은 Mitsubishi Q-series에 그대로 존재하지 않습니다. 다음 두 가지로 매핑해야 합니다.

 

의사코드 Q-series 실제 IL 동작
CRC SHR 1 (우시프트 1회) SFR D K1 D 를 우측으로 1비트 시프트. 시프트로 밀려난 bit0 이 캐리 플래그 SM700 에 보존됨
CRC AND 0x0001 (bit0 검사) AND SM700 직전 SFR 로 밀려난 bit0 을 SM700 으로 읽음. 주의 — AND K1 은 Q-series에서 K1을 정수가 아닌 접점으로 해석하여 항상 ON, 잘못된 사용

 

🛑 위험

AND K1 으로 bit0 을 검사하려는 시도는 Q-series에서 K1 을 상수가 아니라 접점 번호(M1·X1 등과 같은 디바이스)로 해석해 항상 ON 이 됩니다. CRC 계산이 통째로 틀어지는 대표적 함정이므로, 반드시 SFR 직후의 캐리 플래그 SM700 으로 분기하세요.

 

SM700 캐리 플래그 동작 흐름

CRC 레지스터에 SFR 우시프트를 실행하면 두 가지가 동시에 일어납니다.

 

  • CRC 레지스터 자체가 오른쪽으로 1비트 이동합니다.
  • 밀려난 bit0 (시프트 전의 최하위 비트)이 시스템 캐리 비트 SM700 에 보존됩니다.

 

따라서 시프트 직후에 SM700 을 읽으면 시프트 전 bit0 값을 알 수 있습니다. 이 값으로 "다항식 0xA001 을 XOR 할지 시프트만 유지할지" 분기하면 됩니다.

래더로 표현하면 다음과 같습니다.

 

 [시프트 트리거]───────────────[ CRC 우시프트 1회 (밀려난 bit0 → 캐리 비트) ]

 [캐리 비트 ON]─────────────────────────────────( 다항식 0xA001 XOR 실행 )

 [캐리 비트 OFF]────────────────────────────────( 시프트만 유지 — 그대로 다음 회차 )

 

SM700 은 시프트·로테이트 명령(SFR/SFL/RCR/RCL) 실행 직후의 캐리 상태를 보존하는 시스템 비트이므로, 다음 시프트 명령이 실행되기 전에 값을 확인하고 분기해야 합니다. 시프트가 한 번 더 실행되면 SM700 값이 새로 덮어쓰여 이전 값은 사라집니다.

두 겹 루프 구조

전체 흐름을 두 겹 루프로 정리하면 다음과 같습니다.

외부 루프 — 바이트 누적 (국번 ~ CRC 직전, 바이트 수만큼 반복)

 

  1. 인덱스 레지스터로 현재 처리할 바이트 번지를 추적합니다.
  2. 해당 바이트와 CRC Low Byte 를 XOR 하여 CRC Low Byte 에 다시 저장합니다.
  3. 내부 루프(8회 비트 시프트)를 실행합니다.

 

내부 루프 — 비트 시프트 (K8 고정, 8회 반복)

 

  1. CRC 우시프트 1회 (SFR CRC K1) — 동시에 SM700 에 시프트 전 bit0 보존.
  2. SM700 ON 이면 CRC 에 다항식 0xA001 을 XOR.
  3. SM700 OFF 이면 시프트 결과를 그대로 유지.

 

내부 루프의 핵심 분기를 래더로 표현하면 다음과 같습니다.

 

 [비트 처리 회차 < 8]───────────────[ CRC 우시프트 1회 (캐리 비트 갱신) ]

 [비트 처리 회차 < 8]─[캐리 비트 ON]──────────( 다항식 0xA001 XOR )

 [비트 처리 회차 < 8]─[캐리 비트 OFF]─────────( 시프트만 유지 )

 

내부 루프 3줄(시프트·XOR 분기·시프트만 유지)을 8회 반복하면 한 바이트의 비트 처리가 끝납니다. 외부 루프가 바이트 수만큼 반복되면 전체 CRC 계산이 완료됩니다.

Modbus 표준과의 정합: refin/refout/poly/init

Modbus CRC-16 은 다음 변형 조합에 속합니다.

 

항목 Modbus 사양
초기값 (init) 0xFFFF
다항식 (poly) 0xA001 (= 표준 0x8005 의 reflected 형)
입력 반전 (refin) True (LSB-first 처리)
출력 반전 (refout) True
XOR 출력 0x0000

 

산업 현장에 적용되는 CRC-16 라이브러리는 refin·refout·poly·init 조합으로 16종 변형을 지원합니다. Modbus 외에도 CRC-16/CCITT, CRC-16/ARC, CRC-16/USB 등 다양한 표준이 있고, 각 표준마다 SFR (refin=True) 또는 SFL (refin=False, MSB-first) 를 골라 써야 합니다. SFL 사용 시에는 SM700이 bit15(MSB) 를 캡처합니다.

계산 결과 비교

계산이 끝나면 CRC 레지스터 최종값의 Low Byte 와 High Byte 를 수신 프레임 마지막 2바이트와 각각 비교합니다.

 

CRC 계산 결과 Low Byte  =  ZR6_CRC_LAST - 1 번지
CRC 계산 결과 High Byte =  ZR6_CRC_LAST 번지

 

두 값이 모두 일치하면 정상 프레임으로 처리합니다.

검증 결과 분기: 래더 표현

CRC 검증 결과에 따라 데이터 처리·재요청 두 갈래로 갈리는 부분을 래더로 그리면 다음과 같습니다.

 

 [수신 완료]──[CRC 일치]────────────────────────────────( 데이터 처리 OK )

 [수신 완료]──[CRC 불일치]───────────────────────────────( 재요청 요구 )

 

  • 왼쪽부터 입력 접점([조건])들이 직렬로 연결되고, 모든 조건이 만족되면 오른쪽 코일(( 출력 ))이 ON 됩니다.
  • 위 두 줄은 같은 [수신 완료] 조건을 공유하지만, CRC 검증 결과에 따라 한쪽 코일만 ON 됩니다.
  • 코일 출력을 받아 데이터 추출 단계(BTOW)로 가거나, 수신 버퍼를 클리어하고 재요청 송신 시퀀스로 분기시키면 됩니다.

 

3. BTOW: 바이트를 워드로 복원하기

CRC 검증까지 통과했다면 이제 데이터를 추출할 차례입니다.

WTOB 로 분해한 바이트 배열에서 데이터 영역(ZR6203~)을 꺼내 실제로 사용할 워드 형태로 복원합니다. 이것이 BTOW 명령입니다.

 

바이트 0x00, 0xF5  →  BTOW  →  워드 0x00F5 (= 245)
바이트 0x01, 0x40  →  BTOW  →  워드 0x0140 (= 320)

 

📝 NOTE

Modbus 는 데이터를 빅엔디안(Big-Endian) 으로 전송합니다. High Byte 가 먼저 오고 Low Byte 가 나중에 옵니다. BTOW 는 이 순서대로 워드를 합성해주므로 바이트 순서를 따로 바꿀 필요 없이 그대로 사용하면 됩니다.

 

복원된 워드 값에 필요하다면 스케일 처리를 추가합니다.

예를 들어 온도 컨트롤러가 온도값을 10배 스케일로 보낸다면 (245 = 24.5°C), 복원된 워드를 10으로 나누어 사용합니다.

BTOW 호출 조건: 래더 표현

BTOW 도 무조건 매 스캔 실행하면 안 됩니다. CRC 검증을 통과한 시점에만 한 번 실행합니다.

 

 [수신 완료]──[CRC 일치]────────────────( BTOW 워드 복원 실행 )

 

CRC 검증 결과 분기(§2 끝)의 "데이터 처리 OK" 출력과 같은 조건. 즉 데이터 처리 단계 안에서 BTOW 가 가장 먼저 호출됩니다.

4. 전체 파싱 흐름

정리하면 수신 데이터 처리 전체 흐름은 이렇습니다.

 

BMOV 로 수신 데이터 복사 완료
    ↓
WTOB → 워드 배열을 바이트 배열로 분해
    ↓
국번 (ZR6200) 확인 → 요청 슬레이브 번호와 일치?
    ↓
FC (ZR6201) 확인 → MSB = 0 (정상) / MSB = 1 (Exception)
    ↓
CRC-16 계산 (FOR~NEXT 이중 루프)
    ↓
수신 CRC 와 비교 → 일치?
    ↓
BTOW → 데이터 바이트를 워드로 복원
    ↓
내부 레지스터에 최종 적재
    ↓
G168 (CH1) / G328 (CH2) SET → 수신 버퍼 클리어
    ↓
다음 요청 송신 대기

 

전체 흐름: 단계별 래더 표현

위 텍스트 다이어그램을 래더로 풀면 각 단계가 직전 단계 완료 신호를 받아 순차 실행되는 구조입니다.

 

 [수신 완료]──[BMOV 복사 완료]────────────────────( WTOB 바이트 분해 실행 )

 [WTOB 완료]──[국번 일치]──[FC 정상]──────────────( CRC 검증 단계 시작 )

 [CRC 검증 완료]──[CRC 일치]──────────────────────( BTOW 워드 복원 실행 )

 [CRC 검증 완료]──[CRC 불일치]────────────────────( 재요청 + 수신 버퍼 클리어 )

 [BTOW 완료]──────────────────────────────────────( 데이터 적재 + 수신 버퍼 클리어 )

 [수신 버퍼 클리어 완료]──────────────────────────( 다음 요청 송신 트리거 )

 

각 단계는 직전 단계의 완료 신호와 검증 조건을 직렬로 받아 다음 출력을 ON 시킵니다. 검증 실패 시(국번 불일치·FC Exception·CRC 불일치) 정상 경로를 차단하고 재요청 경로로 분기합니다.

마무리

2편에서는 수신 데이터를 실제로 사용할 수 있는 형태로 가공하는 전 과정을 살펴봤습니다.

핵심을 한 줄로 요약하면 이렇습니다.

WTOB 분해 → 국번·FC 검증 → CRC-16 검증 → BTOW 복원

이 흐름을 이해하면 Modbus RTU 는 물론이고, 다른 바이너리 기반 시리얼 통신 프로토콜도 같은 방식으로 접근할 수 있습니다.

다음 편에서는 이 CRC 검증·파싱 구조가 실제로 동작하는 토대인 QJ71C24N 파라미터 설정, 수신 종료 판정, BMOV 복사, 버퍼 초기화 래더 구현을 다룹니다.

 

💡 TIP

무수순 교신으로 Modbus RTU 송수신 전체 시퀀스를 구성하는 과정은 Modbus 입문 가이드 3편: QJ71C24N 무수순 통신으로 RTU 구현 에서 이어집니다. 본 편에서 다룬 CRC·파싱 구조가 실제 송수신 흐름(파라미터 설정·수신 종료 판정·버퍼 초기화) 안에서 어떻게 맞물리는지 확인할 수 있습니다.

 

💡 TIP

본 편의 CRC-16 래더를 매번 새로 작성하지 않고 XCALL 재사용 함수로 라이브러리화한 버전도 있습니다. Modbus 전용 CRC-16 은 함수화 6편: CRC-16/MODBUS 코드 생성, CCITT·ARC 등 다항식까지 가변 지원하는 범용 버전은 함수화 7편: 모든 CRC-16 코드 생성 에서 다룹니다. 원리는 이 글에서, 실무 재사용은 함수편에서 익히면 됩니다.

 

반응형